What happens to the value returned from the function passed to QueueUserWorkItem?
QueueUserWorkItem function schedules a function to be called from a thread pool thread. It’s one of the so-called legacy thread pool functions, a group of functions introduced in Windows 2000 in the first version of the system thread pool. It has since been superseded by the new thread pool functions like
CreateThreadpoolWork, but the old functions continue to work, for compatibility. (They are implemented as wrappers around the new thread pool functions.)
QueueUserWorkItem function takes a
LPTHREAD_ function pointer as the function to run on a thread pool thread. This is curious because the
LPTHREAD_ function returns a
DWORD. What does the system do with the
DWORD returned by a work item?
The return value is simply discarded.
It doesn’t matter what you return, as long as you return something. (And do make sure you return something. Don’t just fall off the end of the function and return uninitialized garbage, because that uninitialized garbage could be deadly.)
Mind boggles why the garbage is deadly. When ANSI C was new, it allowed falling off the end of an int function so long as the caller didn’t check. Even itamium only faulted when you wrote the value to memory, which you won’t because it’s call-clobbered.
If anything, we’re all spoiled these days because the compiler will generally just go ahead and insert a “return 0;” if you just fall off the end of an int-defined function. You have to work pretty hard to corrupt the stack on accident now.
I thought modern compilers are more aggressive in exploiting undefined behaviors?
If I’m not mistaken, your statement is only applicable to the main function.
Practically speaking it’s limited how undefined it can get because the compiler can’t see the call site in this case. It needs to emit the function as if it was going to be called with the signature you claimed. The cast itself is valid as long as its cast back to a function pointer compatible with the signature, and since this is happening in an external dll the compiler won’t know that there is any UB.
You’re right that at a specific ABI boundary there is no notion of “undefined behavior”; everything is defined by registers and memory.
However, it doesn’t apply to the case of undefinedness that is already on the language level. For example, inside an int-returning function the compiler is free to assume that the closing curly brace is never reached (since that would be UB) and thus for
the compiler may choose not to generate even a
retinstruction (since it would be “unreachable” anyways).
On the topic of function pointer casting then, observe that it’s still language-level UB since the correct signature of the function has been declared in the translation unit, and the cast into a contradictory function pointer type is expressed in language-level expressions.
If you really want to exploit the ABI property of function signature compatibility then, you may have to employ one of 2 ways to circumvent the need for a language-level FP cast, both involving separate compilation:
void-returning function separately, and yet declare it in the consuming file as returning
DWORD, and then take its address directly;
QueueUserWorkItemseparately (which it already has been), and yet declare
QueueUserWorkItemas then pass your function to it directly.
Either way there is no suspicious code at the language level, and all fishiness exists at the ABI boundary. Of course those would still only happen to work under a “dumb” linker; they could break under sufficiently sophisticated LTO for example.
> On the topic of function pointer casting then, observe that it’s still language-level UB since the correct signature of the function has been declared in the translation unit, and the cast into a contradictory function pointer type is expressed in language-level expressions.
Casting between incompatible function pointers isn’t UB in C, it is only UB when the function is called through an incompatible function pointer type.
From ISO/IEC 9899:2018 Section 220.127.116.11, paragraph 8:
> 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.
which is not what we’re doing here.
I’m not sure what “compatible” means here; it could mean ABI-compatible or type-identical or something else. Unfortunately I couldn’t easily find an answer to it on the internet.
Compatible type is defined in C99 §6.2.7, with references to 6.7.2, 6.7.3, and 6.7.5. It basically means the same type, but with a lot of additional pedantic rules (e.g. enumerations are compatible with an implementation-defined integer type).