Some of the sockets/unix networking struct
objects behave and are intended to be used as if they were a union
type. In other words, many functions in the sockets API take a struct sockaddr*
, and it is expected that the client will call these functions with a range of struct
types by casting pointers to different types.
For example, a sockaddr_in
(ipv4) and sockaddr_in6
(ipv6) struct may be cast to a struct sockaddr*
by taking the address of the object and performing a pointer type cast.
I am curious to know why there is no explicit union
type involved here, even though these objects are intended to be used and behave as if they were a union
type.
I hope the question makes sense and is clear enough? This is a somewhat abstract or difficult to describe question.
union
behave somewhat polymorphically. Perhaps this comment helps to explain what I am thinking about here.
Some of the sockets/unix networking struct
objects behave and are intended to be used as if they were a union
type. In other words, many functions in the sockets API take a struct sockaddr*
, and it is expected that the client will call these functions with a range of struct
types by casting pointers to different types.
For example, a sockaddr_in
(ipv4) and sockaddr_in6
(ipv6) struct may be cast to a struct sockaddr*
by taking the address of the object and performing a pointer type cast.
I am curious to know why there is no explicit union
type involved here, even though these objects are intended to be used and behave as if they were a union
type.
I hope the question makes sense and is clear enough? This is a somewhat abstract or difficult to describe question.
union
behave somewhat polymorphically. Perhaps this comment helps to explain what I am thinking about here.
5 Answers
Reset to default 6The sockets API is very old, dating back to the 1980s. The ANSI C standard was ratified only in 1989. Programmers using C were not concerned with academic undefined behavior issues, like that certain type punning is formally well-defined if done through a union
, but not otherwise.
C unions are cumbersome to use, due to introducing extra members. I think ISO C still does not have transparent unions.
If you make a union
out of all the possible structures, the resulting object will have a size large enough to contain all of them. That may be undesirable.
Only fairly recently (in terms of the long history of the API) did POSIX add the type struct sockaddr_storage
which has this property of being large enough to define storage for any sockaddr
type, but that's normally only used for that specific purpose.
Code not working with any type might not want the overhead of the extra storage not needed by its type.
The API designers opted for the simple "OOP" technique of just casting pointers from a "derived" class like struct sockaddr_in *
to a "base" type like struct sockaddr
, ensuring that any common members were put in front, so that for instance the address family could be accessed through any type to tell that the struct sockaddr
is really struct sockaddr_in
, due to its tag being AF_INET
.
This is not unheard of C programs, not just the socket API of POSIX.
In addition to what the other answers have said, many new protocols have appeared since the early days when sockaddr
was introduced. Those protocols use their own unique sockaddr_...
struct types (ie, sockaddr_un
for UNIX domains, sockaddr_bth
for Bluetooth, etc), sometimes in separate libraries/headers, but they are still "compatible" with socket APIs that take basic sockaddr*
pointers. It would be much more difficult to develop new protocols if all of the structs had to be declared in a single union
in one place.
It proved to be tremendous future proofing; IPv6 migration would have been monstrous had that not been the case. As it was we only had to retrofit for the new IPv6 type in a handful of places. Code that did what it was supposed to and call gethostbyname()
or getaddrinfo()
required no conversion for IPv6. And this is because these structs weren't unions so these library functions could just allocate larger structs when encountering an IPv6 address and the code actually worked.
Prior to C99, the C language had unambiguously guaranteed that if two or more structures have matching types for the first N members, a function that uses a pointer of any of those types only for the purpose of inspecting some or all of the first N members may be passed a suitably-cast pointer to any object of any of those types, and will access the corresponding members of the structure passed to it, at least if the alignment of the passed object is compatible with the alignment requirement of the type used to access it. This behavior was documented going back at least to 1974.
The authors of C99 decided to change the rules to require that a complete definition of a union type containing any involved structures be visible in parts of the code that exploit the Common Initial Sequence guarantees(*), which might have resulted in code being modified to add such union type definitions if any compilers actually required them. So far as I can tell, however, the only any compiler configuration that would process code incorrectly without such union declarations would interpret it the same way even if such a declaration were present.
Note that C99 allows implementations to continue processing the language the Committee was chartered to describe; what changed is that it allows implementations that are intended exclusively for tasks that wouldn't benefit from CIS guarantees to process a dialect that lacks a feature that had for 25 years been part of full-featured C.
(*) The purpose of this was to allow a compiler given something like:
for (int i=0; i<100; i++)
{
positions[i].x += velocities[i].dx;
positions[i].y += velocities[i].dy;
}
to use vector operations that would read multiple velocities at once, and add them to multiple positions, without having to accommodate the possibility that the address of positions[0]
might correspond with the address of velocities[1]
, causing that the value of velocities[1].dx
during the second iteration of the loop to be different from what it had been before position[0]
was written. If velocities use a different structure type from positions, compilers could use the revised rule to justify ignoring the possibility of such overlap if they were intended for use in situations where compatibility with code exploitiung CIS guarantees was less important than the potential performance benefits the rules could offer.
The text regarding complete union type definitions was most likely a result of someone observing that there should be a means of informing a compiler that code was relying upon the Common Initial Sequence being usable with certain combinations of structures, and someone (possibly that person or someone else) observing that the existing union definition syntax could serve that purpose. Programs wouldn't usually define a union type containing two or more structures if it wouldn't be performing type punning between those types, and compilers that would honor the Common Initial Sequence guarantees could simply process union type definitions as simply defining union types. This might have been a usable comrpomise if compiler writers didn't invent their own non-standard syntax for the purpose and demand that programmers use that instead of the means provided by the Standard.
This is because the number of address families is not known at compilation time... so a union
should require all of them to be known at compilation. There is a generic one, struct sockaddr
with only the common fields. All the other structs, each belong to a different sockets implementation.
sockaddr_*
types are spread over many headers. Keep in mind also that this is a design from the 1980s, when we hadn't yet learned that a bunch of things are bad ideas, and networking looked very different from how it does now. – zwol Commented Feb 4 at 22:31