Opaque structure with flexible array member

2019-08-16 16:52发布

问题:

Suppose I have a struct declaration in a header file like:

event.h

struct event_t;

and in the corresponding C file I would like to sort-of alias it with the Linux-specific struct inotify_event. The problem is that struct inotify_event contains flexible array member:

struct inotify_event {
    int      wd;      
    uint32_t mask;    
    uint32_t cookie;  
    uint32_t len;
    char     name[];   
};

As per 6.7.2.1(p3) (emphasize mine):

A structure or union shall not contain a member with incomplete or function type (hence, a structure shall not contain an instance of itself, but may contain a pointer to an instance of itself), except that the last member of a structure with more than one named member may have incomplete array type; such a structure (and any union containing, possibly recursively, a member that is such a structure) shall not be a member of a structure or an element of an array.

it is not possible to define the struct event_t as

struct event_t{
    struct inotify_event base; //Non-conforming
};

So I could convert struct event_t * to struct inotify_event *. Since the 6.7.2.1(p3) concerns only about structs the solution I see is to redeclare the tag name as

union event_t

and then define it later as a single element union.

union event_t{
    struct inotify_event event; //Conforming?
};

The only requirement the Standard imposes on union that I found is that the set of members of a union must be non-empty 6.2.5(p20) (emphasize mine):

A union type describes an overlapping nonempty set of member objects, each of which has an optionally specified name and possibly distinct type.

QUESTION: Is it a conforming/common way to hide an implementation details of some specific data structure through union?

回答1:

By far the simplest technique is to put this into your event.h header:

typedef struct inotify_event event_t;

This declares that there is a structure type struct inotify_event and declares an alias for it event_t. But it does not define the content of struct inotify_event at all.

Only the implementation code in event.c includes the definition of struct inotify_event from the system header; everything else does not include that header and cannot access the elements of an event_t except through the accessor API you define.

You can enforce this separation of duties by code review — or by checking with grep, or other similar techniques — to ensure that no code except the implementation of your event type uses the system header for inotify_event. And, if you port to a system other than Linux without support for inotify, then you simply provide an alternative opaque structure type in place of struct inotify_event in your event.h header.

This avoids all questions about whether there are flexible array members within structures, etc; it is all a non-issue.


Note the Q&A about What does a type followed by _t (underscore t) represent? . Be cautious about creating your own types with the _t suffix ¸— consider using a prefix on such type names that gives you a chance that your names will be distinct from those provided by the system.



回答2:

This is how I would do it:

event.h

struct event_t;
event_t *create_event(void);
void free_event(event_t *ev);

event.c

#include "event.h";

event_t *create_event(void)
{
    inotify_event *iev = ...;
    return (event_t *)iev;
}

void free_event(event_t *ev)
{
    inotify_event *iev = (inotify_event *)ev;
    // free the event
}

However, if you want to store additional data with the event then:

event.h

struct event_t;
event_t *create_event(void);
void free_event(event_t *ev);

event.c

#include "event.h";

struct event_t
{
    inotify_event *iev;
    // additional data
};

event_t *create_event(void)
{
    inotify_event *iev = ...;
    event_t *ev = malloc(sizeof(event_t));
    ev.iev = iev;
    return ev;
}
void free_event(event_t *ev)
{
    inotify_event *iev = (inotify_event *)ev.iev;
    // free the event (iev) first
    free(ev);
}

If you have multiple implementations that you need to hide in event_t then:

enum event_type
{
    EVENT_TYPE_INOTIFY,
    EVENT_TYPE_INOTIFY2,
};
struct event_t
{
    event_type type;
    union {
       inotify_event *iev; // you use this when type == EVENT_TYPE_INOTIFY
       inotify_event2 *iev2; // you use this when type == EVENT_TYPE_INOTIFY2
    }
    // additional data
};


回答3:

Single element union makes no sense. The purpose of union is to serve as a kind of polymorphic struct. struct members are accessed by offset, this is why is impossible to put an incomplete struct or array in the middle of a struct.

For example

struct foo { int a; int b[]; int c; };

In this example is impossible for the compiler to determine the address of c because b size can vary at runtime. But if you put incomplete array at the end all struct members address can be determined by the address of the start of the struct. Keep in mind that pointers are just address, so you can have any pointers to any structs and all the offsets can be determined, but you will need to deal with extra alloc/free stuff.

When you create an union you telling to compiler Hey! I have this members, reserve enough space for me so that I can treat this variable as foo or bar. In another words, the compiler will take the largest union member and this will be the size of the union. A common use for union is for representing multiple kinds of values.

typedef union { int integer, float real, char *string } value_type;

This way you can treat value_type as int, float, or a char pointer. You're code need to know the how to treat each member but the compiler will make sure that when you do malloc(sizeof value_type) you have enough space for the tree types.

Now your problem. You want to hide implementation details. Usually this is done by declaring a type or struct incompletely in a header, and completely only on your object files. Because of this when the user include your header all the information that the compiler has is struct my_struct;. It can't tell the size of my_struct so is impossible for you to allocate it as malloc(sizeof struct my_struct). Also since the user hasn't the member definitions it can't mess up with the struct internals.

Working like this you will need to provide user with functions for allocating and freeing my_struct, for example struct my_struct *foo = my_struct_new() and my_struct_destroy(foo).

You're already doing this. To deal with the struct inotify problem I would do one of these.

(1) Surround OS specific with #ifdef for that OS, so that the event_t has only the right members defined depending on the operating system. You will need #ifdef on your functions. This has the advantage to keep useless code out of final binary, so smaller footprint.

(2) Have pointers to OS specific structs and let runtime decide what to do. This easier to maintain.



标签: c linux struct