I was trying to implement GLOB_ALTDIRFUNC
last night, and tripped into an interesting question.
While maybe slightly semantically different, are (void *)
and (struct *)
types equivalent?
Example code:
typedef struct __dirstream DIR;
struct dirent *readdir(DIR *);
DIR *opendir(const char *);
...
struct dirent *(*gl_readdir)(void *);
void *(*gl_opendir)(const char *);
...
gl_readdir = (struct dirent *(*)(void *))readdir;
gl_opendir = (void *(*)(const char *))opendir;
...
DIR *x = gl_opendir(".");
struct dirent *y = gl_readdir(x);
...
My intuition says so; they have basically the same storage/representation/alignment requirements; and they should be equivalent for arguments and return type.
Sections 6.2.5 (Types) and 6.7.6.3 (Function declarators (including prototypes)) of the c99 standard and the c11 standard seem to confirm this.
So the following implementation should in theory work:
Now I see similar things being done in BSD and GNU libc code, which is interesting.
Are the equivalence of these conversions a result of an implementation artifact from the compilers, or it is a fundamental restriction/property that can be inferred from the standard's specification?
Does this result in undefined behavior?
@nwellnhof said:
For two pointer types to be compatible, both shall be identically qualified and both shall be pointers to compatible types.
Ok, this is the key. How can (void *)
and (struct *)
be incompatible?
From 6.3.2.3 Pointers: A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the pointed-to type, the behavior is undefined.
Not yet determined.
Further clarification:
DIR
anywhere, but it's guaranteed to be a struct.Examples in the wild, in this very feature:
_GNU_SOURCE
-> __USE_GNU
).So, my thoughts so far are:
(struct *)
with a (void *)
, and the other way around.struct
would have the alignment of the first element, and this could be a char
so requirements of a pointer are exactly the same as for void
pointers.struct
pointer should have the same implementation requirements as a void
pointer, which is further reinforced by the requirement for all struct
pointers to be equivalent.(const void *)
should be equivalent to (const struct *)
(void *)
should be equivalent to (struct *)
Considering ISO C alone: section 6.3.2.3 specifies which casts among pointer types are required not to lose information:
- A pointer to any object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer.
- A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned for the referenced type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer.
- A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the referenced type, the behavior is undefined.
(emphasis mine) So, let's look at your code again, adding in some of the declarations from dirent.h
:
struct dirent;
typedef /* opaque */ DIR;
extern struct dirent *readdir (DIR *);
struct dirent *(*gl_readdir)(void *);
gl_readdir = (struct dirent *(*)(void *))readdir;
DIR *x = /* ... */;
struct dirent *y = gl_readdir(x);
This casts a function pointer of type struct dirent *(*)(DIR *)
to a function pointer of type struct dirent *(*)(void *)
and then calls the converted pointer. Those two function pointer types are not compatible (in most cases, two types must be exactly the same to be "compatible"; there are a bunch of exceptions but none of them apply here) so the code has undefined behavior.
I want to emphasize that "they have basically the same storage/representation/alignment requirements" is NOT enough to avoid undefined behavior. The infamous sockaddr
mess involves types with the same representation and alignment requirements, and even the same initial common subsequence, but struct sockaddr
and struct sockaddr_in
are still not compatible types, and reading the sa_family
field of a struct sockaddr
that was cast from a struct sockaddr_in
is still undefined behavior.
In the general case, to avoid undefined behavior due to incompatible function pointer types, you have to write "glue" functions that convert back from void *
to whatever concrete type is expected by the underlying procedure:
static struct dirent *
gl_readdir_glue (void *closure)
{
return readdir((DIR *)closure);
}
gl_readdir = gl_readdir_glue;
GLOB_ALTDIRFUNC
is a GNU extension. Its specification was clearly (to me, anyway) written back in the days when nobody worried about the compiler optimizing based on the assumption that undefined behavior could never occur, so I do not think you should assume that the compiler will Do What You Mean with gl_readdir = (struct dirent *(*)(void *))readdir;
If you are writing code that uses GLOB_ALTDIRFUNC
, write the glue functions.
If you are implementing GLOB_ALTDIRFUNC
, just store the void *
you get from the gl_opendir
hook in a variable of type void *
, and pass it directly to the gl_readdir
and gl_closedir
hooks. Don't try to guess what the caller wants it to be.
EDIT: The code in the link is, in fact, an implementation of glob
. What it does is reduce the non-GLOB_ALTDIRFUNC
case to the GLOB_ALTDIRFUNC
case by setting the hooks itself. And it doesn't have the glue functions I recommended, it has gl_readdir = (struct dirent *(*)(void *))readdir;
I wouldn't have done it that way, but is true that this particular class of undefined behavior is unlikely to cause problems with the compilers and optimization levels that are typically used for C library implementations.