3

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:

  • Structs depend on their first element for alignment, so alignment requirements for a struct pointer should be the same as for void pointers, right?
  • I didn't initially specify DIR anywhere, but it's guaranteed to be a struct.
  • The whole point of the question is to know if I can avoid wrappers (like the one I did for gl_closedir, whose type is clearly incompatible).
  • While this may not be allowed by C11/C99, it is in practice used by BSD and GNU system code, so maybe some other relevant standard, e.g. POSIX, specifies the behavior.

Examples in the wild, in this very feature:

So, my thoughts so far are:

  • I can't think of any reason you can't exchange a (struct *) with a (void *), and the other way around.
  • A 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.
  • So a 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.
  • So (const void *) should be equivalent to (const struct *)
  • and (void *) should be equivalent to (struct *)
  • and so on with all special attributes.
Ismael Luceno
  • 2,055
  • 15
  • 26
  • 2
    You'll probably get a better response if you replace some of your tags with the "C" and "language-lawyer" tags, especially the "C" tag. – Andrew Henle Jul 31 '19 at 11:23
  • 2
    Your question vague on what equivalence you are interested in, and some of the code is presumably hidden behind external links. Please write simple [mcve] so we can see exactly what the answer should be. – user694733 Jul 31 '19 at 11:55
  • @user694733 thanks, I should add some more code... I thought it way obvious, but maybe not. – Ismael Luceno Jul 31 '19 at 12:02
  • Please post a MRE as suggested by the previous comment. `void *` and `struct X *` are not compatible types. glibc is written to be compiled by gcc, it makes no pretense at being portable or standard-conforming – M.M Jul 31 '19 at 12:25
  • I added declarations of `readdir` and `opendir` to clarify the question. If there is a mistake, please edit or rollback the question. – user694733 Jul 31 '19 at 12:29
  • `The readdirfunc declaration can't be prototyped` this is a strange comment from openssh-portable/glob.c . A simple `static struct direcnt * conv_readdir_to_proper_pointer(void *pnt) { return readdit(pnt); }` would suffice to handle the pointer. They could even `while ((dp = (pglob->gl_flags & GLOB_ALTDIRFUNC) ? pglob->gl_readdir(dirp) : readdir(dirp)))`. – KamilCuk Jul 31 '19 at 13:29

3 Answers3

3

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.

zwol
  • 135,547
  • 38
  • 252
  • 361
  • please see the comments, I wasn't asking for a workaround, I was asking if this kind of hack is reliable because some standard specifies it in some way, even if obscure. – Ismael Luceno Jul 31 '19 at 12:59
  • 1
    @IsmaelLuceno I believe I am saying that this kind of hack is _not_ to be relied on. – zwol Jul 31 '19 at 13:00
  • well, that's a trivial guess I would have been happy with if I were lazy, but I'm here to find out if it's just a widespread bug or it is in fact something reliable :-P. – Ismael Luceno Jul 31 '19 at 13:06
  • you have to admit the reduced indirection is desirable. – Ismael Luceno Jul 31 '19 at 13:07
  • also, again, this _is being used_ by several implementations. – Ismael Luceno Jul 31 '19 at 13:10
  • 4
    @IsmaelLuceno My honest opinion is that all of those implementations are buggy, and also that there isn't anything more we, collectively, can quote at you; everything I might have said but didn't appears to be covered by the other answers. – zwol Jul 31 '19 at 13:14
2

From the C99 standard, 6.7.5.1 Pointer declarators:

For two pointer types to be compatible, both shall be identically qualified and both shall be pointers to compatible types.

So void * and DIR * aren't compatible.

From 6.7.5.3 Function declarators (including prototypes):

For two function types to be compatible, both shall specify compatible return types. Moreover, the parameter type lists, if both are present, shall agree in the number of parameters and in use of the ellipsis terminator; corresponding parameters shall have compatible types.

So struct dirent *(*)(void *) (the type of gl_readdir) nd struct dirent *(*)(DIR *) (the type of readdir) aren't compatible.

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.

So

gl_readdir = (struct dirent *(*)(void *))readdir;
gl_readdir(x);

is undefined behavior.

nwellnhof
  • 32,319
  • 7
  • 89
  • 113
  • This answer avoids to explain _why_, you just assert they're incompatible, but where's the proof? proving that is a lot easier than proving they're compatible, just a counterexample does. – Ismael Luceno Jul 31 '19 at 12:50
  • 2
    @IsmaelLuceno If the quoted text from section 6.7.5.3 is not sufficient, please explain what exactly would qualify as "proof" to you. In language-lawyer questions, that is usually the kind of proof we go for. – zwol Jul 31 '19 at 13:05
  • well, basically that you actually can implement a valid compiler where `void*` and `struct*` can not be exchanged succesfully... – Ismael Luceno Jul 31 '19 at 13:39
1

struct x* and struct y* for any two x and y are guaranteed to have the same representation and alignment requirements, same for union pointers, but not void pointers and struct pointers:

http://port70.net/~nsz/c/c11/n1570.html#6.2.5p28

A pointer to void shall have the same representation and alignment requirements as a pointer to a character type.48) Similarly, pointers to qualified or unqualified versions of compatible types shall have the same representation and alignment requirements. All pointers to structure types shall have the same representation and alignment requirements as each other. All pointers to union types shall have the same representation and alignment requirements as each other. Pointers to other types need not have the same representation or alignment requirements.

Furthermore, same representation and alignment requirements of a function type's "subtypes" isn't enough. For a call through a function pointer to be defined, the function pointer's target type must be compatible with the actual function's type and for function compatibility, strict compatibility between corresponding function arguments is required, which means that technically e.g., void foo(char*); is not compatible with void foo(char const*); even if char* and char const* have the same representation and alignment.

http://port70.net/~nsz/c/c11/n1570.html#6.7.6.3p15

For two function types to be compatible, both shall specify compatible return types.146) Moreover, the parameter type lists, if both are present, shall agree in the number of parameters and in use of the ellipsis terminator; corresponding parameters shall have compatible types. If one type has a parameter type list and the other type is specified by a function declarator that is not part of a function definition and that contains an empty identifier list, the parameter list shall not have an ellipsis terminator and the type of each parameter shall be compatible with the type that results from the application of the default argument promotions. If one type has a parameter type list and the other type is specified by a function definition that contains a (possibly empty) identifier list, both shall agree in the number of parameters, and the type of each prototype parameter shall be compatible with the type that results from the application of the default argument promotions to the type of the corresponding identifier. (In the determination of type compatibility and of a composite type, each parameter declared with function or array type is taken as having the adjusted type and each parameter declared with qualified type is taken as having the unqualified version of its declared type.)

Petr Skocik
  • 58,047
  • 6
  • 95
  • 142