
Type Punning Functions in C - heliostatic
http://www.evanmiller.org/type-punning-functions-in-c.html
======
kbenson
Sir, while the gentlemen and ladies of this fine organization are infinitely
pleased with your research into the black magic of the nether regions and the
treatise you have presented from aforesaid research, we would ask you please
be even more strident in your warnings about practicing this subject. It seems
a number of neophytes have disappeared under odd circumstances in the weeks
since you presented your work, and the interruption to the experiments they
were assisting with has become quite burdensome. Additionally, we expect
training and recruitment costs to be quite a bit higher than normal next year,
and as you well know our coffers are not unlimited.

~~~
catnaroek
> black magic of the nether regions

Or plain abstraction violation. “Black magic” gives it an undeserved good
reputation: “I must aspire to be clever enough to understand and actively make
use of this.”

~~~
kbenson
Maybe. Given it relies on the calling convention of the OS you're on, the CPU
architecture you're on, and knowledge of assembly, it could very well be
_both_.

~~~
int_19h
It also relies on implementation-specific behavior in the compiler, since the
latter is not actually required to follow established the convention for
argument passing, so long as it can prove that the only place the function is
called from is the same translation unit.

In fact, so far as I can tell by looking at the first example, the compiler
can circumvent this whole thing by noting that:

1\. DoubleToTheInt is only ever called in the same unit, so it can be inlined
- no need to generate the function.

2\. Pointer to DoubleToTheInt is created, but is then immediately cast to an
incompatible function type, which is undefined behavior - and so it can be
substituted with any random value, say, whatever happens to be in EAX at that
moment (any U.B. is substitutable for any other U.B.!), and the compiler can
still avoid generating the actual function.

------
quotemstr
Here's an example I'm rather proud of. It involves setting getpid(2) as a
signal handler(2) via JNA.

[https://github.com/facebook/buck/commit/48f1e201a966b9e2adab...](https://github.com/facebook/buck/commit/48f1e201a966b9e2adabdde4a7425d0f3989208f)

~~~
DblPlusUngood
Why is setting the signal handler for SIGHUP to getpid preferable to masking
the signal?

~~~
swolchok
> But when we exec a child, the kernel resets any signal not set to `SIG_IGN`
> to `SIG_DFL`, so all our children, atomically, get their `SIGCHLD` handlers
> set to `SIG_DFL` at the moment of creation. Consequently, when either our
> process or the kernel sends `SIGHUP` to our process group, all our children
> die.

~~~
DblPlusUngood
SIG_IGN is not the same as the signal mask.

The result is the same though; I forgot that signal mask is not reset on exec.
Damn POSIX signals!

------
pklausler
If your C compiler doesn't complain bitterly to you about an incompatible
pointer type on that assignment, get a better C compiler. If your C compiler
does complain bitterly about it, heed those warnings or expect no sympathy.

If you write this in production code, and don't get sent home that day in
tears, your company's code review process has failed.

~~~
MaulingMonkey
> If your C compiler doesn't complain bitterly to you about an incompatible
> pointer type on that assignment, get a better C compiler.

The example code uses C style casts, which is C-speak for "fuck off compiler I
know what I'm doing." I'd like to see what warnings your compiler produces.
VS2015 /W4 /ANALYZE produces nothing, and neither do clang nor GCC from the
looks of it.

That people C cast without knowing what they're doing is rather unfortunate
tragedy. That this is a common enough problem, that the Win32 API is littered
with backwards compatability hacks where someone C-casted away ABI differences
turns it into a statistic.

> If you write this in production code, and don't get sent home that day in
> tears, your company's code review process has failed.

Assuming we're talking "well meaning newbie" instead of "sanity hating self-
flagellating freak and/or undefined behavior worshiping cultist", I prefer to
carefully explain that they've invoked undefined behavior, that undefined
behavior leads to crunching to find optimizer induced heisenbugs in the weeks
before we ship, and to impress upon them that this is why we never cast away
even "trivial" and "minor" function pointer differences. And then, when they
ask what the _correct_ thing to do is, point out the API docs and header both
have this little "WINAPI" annotation they can use. Hey! No more casts!
_Easier_ than the casts too. Win/win.

However, if we _are_ talking about the "sanity hating self-flagellating freak
and/or undefined behavior worshiping cultist", I can only recommend nuking the
site from orbit. It's the only way to be sure. If their home isn't part of the
radioactive crater, you didn't use large enough nukes, and your company's code
review process has failed.

~~~
quotemstr
I'd hate to work at a company with a blanket prohibition on dirty tricks like
this. On rare occasions, there are good reasons to use hacks, and I'd hope my
coworkers would at least be open to an attempt at justification. Sometimes
silly newbie mistakes actually end up being the right thing to do in rare,
specialized circumstances and in experienced hands.

~~~
wahern
I can't ever imagine a scenario where a hack _like_ _that_ is acceptable.

If you need something like that, just drop down to assembly. It's more
portable (in the sense that you're coding to an interface), infinitely more
clear about what you're doing and and how you're doing it, much less likely to
break, and when it does break is likely to break loudly.

Even if you've never done a lick of assembly programming (I haven't done much)
it'd still be infinitely preferable, even if you had to support a dozen
architectures with two dozen different function proxies.

"Hack" is a very broad term. By some definitions a significant chunk of extant
code qualifies as a hack. But whatever the definition, a hack still implies
some quantum of legitimacy. There's nothing legitimate in using function type
punning as a way to reorder or control argument values. Not unless this was
1969, where there was precisely one C compiler, used by precisely one guy, and
where such code was expected to be thrown out in short order, never to infect
code that executed in other than identical circumstances.

~~~
caf
The reordering thing is bunk anyway, because if you just define the reordering
function:

    
    
      double IntPowerOfDouble(int power, double base)
      {
          return DoubleToTheInt(base, power);
      }
    

...then gcc with -O1 will optimise IntPowerOfDouble to a plain "call
DoubleToTheInt", and with -O2 will completely inline DoubleToTheInt into it.

------
tathougies
This is undefined behavior, so you're claim that this is 'in C' is blatantly
false. In general, you cannot assume the underlying architecture will pass its
arguments in any particular location.

~~~
kbenson
It's specifically presented as _not_ part of C.

 _I cheated a little bit above — I assumed you 're running code on a 64-bit
x86 PC. If you're on another architecture, the trick above might not work. In
spite of C's reputation for having an infinite number of dark corners, the
int-double-argument-order behavior is certainly not a part of the C standard.
It's a result of how functions are called on today's x86 machines, and can be
used for some neat programming tricks._

~~~
twoodfin
But it is part of the C standard: It's explicitly undefined behavior.

There are varieties of type punning that are legal C, so I agree it's
confusing to present this UB implementation artifact as "Type Punning ... in
C".

Still a neat magic trick, and an interesting explanation of how it works.

~~~
kbenson
In that respect, every possible situation that could happen is part of the
standard, as it separates everything into defined and undefined. Cosmic rays
twiddling bits in memory is part of the standard by UB under that view. While
technically correct, it's useless in practice when taken to this extreme.

~~~
scott_s
twoodfin is correct in saying that it is explicitly undefined behavior: "If a
converted pointer is used to call a function whose type is not compatible with
the pointed-to type, the behavior is undefined." See
[http://stackoverflow.com/questions/188839/function-
pointer-c...](http://stackoverflow.com/questions/188839/function-pointer-cast-
to-different-signature)

Your response would make sense if twoodfin claimed it was _implicitly_
undefined behavior. But that's not the case. The standard clearly addresses
this case.

~~~
_kst_
Except that the C standard explicitly makes no distinction between explicit
and implicit undefined behavior.

    
    
        If a "shall" or "shall not" requirement that appears outside
        of a constraint or runtime constraint is violated, the behavior
        is undefined. Undefined behavior is otherwise indicated in this
        International Standard by the words "undefined behavior" or by
        the omission of any explicit definition of behavior. There is
        no difference in emphasis among these three; they all describe
        "behavior that is undefined".
    

N1570 section 4 paragraph 2.

------
david-given
Incidentally, the codebase I'm looking at right now contains gems like this:

    
    
        /*VARARGS2*/
        int margin_printf (fp, a, b, c, d, e, f, g)
        FILE *fp;
        char *a;
        long b, c, d, e, f, g;
        {
            ind_printf (0, fp, a, b, c, d, e, f, g);
        } /* margin_printf */
    

Called (from a different source file) like this:

    
    
        margin_printf (outfile, length ? "/* %s */\n" : "\n", storage);
    

Okay, so that's K&R C, and it's not actually compiled any more (because I've
been slowly taking this stuff out and replacing it with things that actually
work), but still --- the horror, the horror...

~~~
caf
That kind of hackery was necessary because K&R C didn't give you any
sanctioned way to write your own printf-like functions. ANSI C introduced
<stdarg.h>.

~~~
david-given
Sure. Also, K&R C tended to target platforms with really simple ABIs, where
parameters were always passed on the stack and everything was a machine word;
so you could get away with this kind of thing.

K&R C actually has a minimalist elegance to it that later versions of C lost.
Not that I'm claiming that it's _better_ , of course. But K&R C had a distinct
philosophy to it that was definitely its own.

------
pavlov
_... we saw how register allocation and calling conventions — supposedly the
exclusive concern of assembly-spinning compiler writers — occasionally pop
their heads up in C ..._

I don't know about "occasionally". At least on Windows, calling conventions
used to be a constant headache. The C language default was cdecl, but the
Win32 API default (most of the time) was stdcall. Then there was a variant
(fastcall?) that tried to make use of registers.

Maybe it's all fixed now... But this being Win32, I rather expect they've
managed to accumulate a few more "interesting" gotchas and edge cases as the
platform expanded with 64-bit, WinRT, Universal Windows Platform and whatever.

~~~
quotemstr
Fortunately, in the amd64 Windows world, there's only one calling convention.
(Edit: as cremno points out, there's now a new vectorcall convention. Dammit.)
The 64-bit Windows ABI is a big improvement over the old 32-bit one. Other
improvements are use of unwind tables for SEH and the requirement that all
code be unwindable.

~~~
jheriko
> the requirement that all code be unwindable.

i would not call that an improvement in all cases... the extra code produced
by enabling exceptions, even for the latest compilers, produces enough bloat
and slow down for basically everyone seriously concerned with real-time
performance to turn them off.

small simple functions are forced to have prologue/epilogue, use a frame
pointer and to preserve the return address on the stack. doesn't sound like
much, but if your function does anything simple (float f() { return 1; } bool
g(float a, float b) { return a < b; } etc...) you spend all the time paying
for exceptions instead of the intended functionality.

~~~
quotemstr
You're confusing unwinding mechanisms. The whole point of using unwind tables
is to _avoid_ the need to preserve frame pointers. There zero cost in pure C
code and minimal cost in C++ code just for preserving the ability to unwind.
You absolutely do not need to have extra prologue and epilogue code or to burn
a register for a frame pointer.

~~~
jheriko
from what i understood it only gets rid of those requirements in some
cases....

there is a table i remember in some doc about this, the only reason i listed
from memory was the lack of being able to find it quickly on google

.. its a shame i can't find it. perhaps my memory is imply faulty.

------
jheriko
its a shame this only glosses over the subject, and starts with a highly
misleading example and then continues with plenty more misleading information.

unix/windows are not interchangeable with compilers targetting those
platforms. the article doesn't make the distinction or highlight why it
matters - the OS has nothing to do with calling conventions in this context,
they are just very conveniently the same in this case.

this example doesn't work with vs2010 out of the box because it targets win32
by default... for example. its the compilers, and only the compilers, which
are deciding this. the standard calling convention for the platform is
incidental, and it being identical to the default for the compiler is a wise
design choice for compilers targetting x64, rather than a necessity -
something which is much clearer in the old world of windows and x86 (32-bit)
where cdecl was the default out of the box, and stdcall was what windows api
calls expected.

~~~
cbr
Doesn't the compiler at least need to use the platform-standard argument
convention when calling OS functions?

~~~
inlined
Yes. Every Windows API is prefixed with WINAPI, which is a macro for the
calling convention.

~~~
int_19h
The macro expands to nothing on Win64, though, because it has one standard
calling convention.

(At the ABI boundary, anyway. There's also __vectorcall now. And, of course,
the compiler can use whatever it wants for functions that are not exposed at
the boundary - even arbitrary register assignments based on where the function
is actually called, to minimize the amount of work needed to save registers
etc.)

------
bitwize
I freaking hate you, OP. I really freaking hate you right now. This is an
abomination in the sight of God.

