Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I typedef an implementation-defined struct in a generic header?

Tags:

c

portability

I have a C project that is designed to be portable to various (PC and embedded) platforms.

Application code will use various calls that will have platform-specific implementations, but share a common (generic) API to aid in portability. I'm trying to settle on the most appropriate way to declare the function prototypes and structures.

Here's what I've come up with so far:

main.c:

#include "generic.h"

int main (int argc, char *argv[]) {
    int  ret;
    gen_t  *data;

    ret = foo(data);
    ...
}

generic.h: (platform-agnostic include)

typedef struct impl_t gen_t;

int foo (gen_t *data);

impl.h: (platform-specific declaration)

#include "generic.h"

typedef struct impl_t {
    /* ... */
} gen_t;

impl.c: (platform-specific implementation)

int foo (gen_t *data) {
    ...
}

Build:

gcc -c -fPIC -o platform.o impl.c
gcc -o app  main.c platform.o

Now, this appears to work... in that it compiles OK. However, I don't usually tag my structures since they're never accessed outside of the typedef'd alias. It's a small nit-pick, but I'm wondering if there's a way to achieve the same effect with anonymous structs?

I'm also asking for posterity, since I searched for a while and the closest answer I found was this: (Link)

In my case, that wouldn't be the right approach, as the application specifically shouldn't ever include the implementation headers directly -- the whole point is to decouple the program from the platform.

I see a couple of other less-than-ideal ways to resolve this, for example:

generic.h:

#ifdef PLATFORM_X
#include "platform_x/impl.h"
#endif

/* or */

int foo (struct impl_t *data);

Neither of these seems particularly appealing, and definitely not my style. While I don't want to swim upstream, I also don't want conflicting style when there might be a nicer way to implement exactly what I had in mind. So I think the typedef solution is on the right track, and it's just the struct tag baggage I'm left with.

Thoughts?

like image 389
SirNickity Avatar asked Nov 08 '14 02:11

SirNickity


1 Answers

Your current technique is correct. Trying to use an anonymous (untagged) struct defeats what you're trying to do — you'd have to expose the details of definition of the struct everywhere, which means you no longer have an opaque data type.


In a comment, user3629249 said:

The order of the header file inclusions means there is a forward reference to the struct by the generic.h file; that is, before the struct is defined, it is used. It is unlikely this would compile.

This observation is incorrect for the headers shown in the question; it is accurate for the sample main() code (which I hadn't noticed until adding this response).

The key point is that the interface functions shown take or return pointers to the type gen_t, which in turn maps to a struct impl_t pointer. As long as the client code does not need to allocate space for the structure, or dereference a pointer to a structure to access a member of the structure, the client code does not need to know the details of the structure. It is sufficient to have the structure type declared as existing. You could use either of these to declare the existence of struct impl_t:

struct impl_t;

typedef struct impl_t gen_t;

The latter also introduces the alias gen_t for the type struct impl_t. See also Which part of the C standard allows this code to compile? and Does the C standard consider that there are one or two struct uperms entry types in this header?

The original main() program in the question was:

int main (int argc, char *argv[]) {
    int  ret;
    gen_t  data;

    ret = foo(&data);
    …
}

This code cannot be compiled with gen_t as an opaque (non-pointer) type. It would work OK with:

typedef struct impl_t *gen_t;

It would not compile with:

typedef struct impl_t gen_t;

because the compiler must know how big the structure is to allocate the correct space for data, but the compiler cannot know that size by definition of what an opaque type is. (See Is it a good idea to typedef pointers? for typedefing pointers to structures.)

Thus, the main() code should be more like:

#include "generic.h"

int main(int argc, char **argv)
{
    gen_t *data = bar(argc, argv);
    int ret = foo(data);
    ...
}

where (for this example) bar() is defined as extern gen_t *bar(int argc, char **argv);, so it returns a pointer to the opaque type gen_t.

Opinion is split over whether it is better to always use struct tagname or to use a typedef for the name. The Linux kernel is one substantial body of code that does not use the typedef mechanism; all structures are explicitly struct tagname. On the other hand, C++ does away with the need for the explicit typedef; writing:

struct impl_t;

in a C++ program means that the name impl_t is now the name of a type. Since opaque structure types require a tag (or you end up using void * for everything, which is bad for a whole legion of reasons, but the primary reason is that you lose all type safety using void *; remember, typedef introduces an alias for an underlying type, not a new distinct type), the way I code in C simulates C++:

typedef struct Generic Generic;

I avoid using the _t suffix on my types because POSIX reserves the _t for the implementation to use* (see also What does a type followed by _t represent?). You may be lucky and get away with it. I've worked on code bases where types like dec_t and loc_t were defined by the code base (which was not part of the implementation — where 'the implementation' means the C compiler and its supporting code, or the C library and its supporting code), and both those types caused pain for decades because some of the systems where the code was ported defined those types, as is the system's prerogative. One of the names I managed to get rid of; the other I didn't. 'Twas painful! If you must use _t (it is a convenient way to indicate that something is a type), I recommend using a distinctive prefix too: pqr_typename_t for some project pqr, for example.

* See the bottom line of the second table in The Name Space in the POSIX standard.

like image 191
Jonathan Leffler Avatar answered Oct 06 '22 00:10

Jonathan Leffler