What happens to the value returned from the function passed to Queue­User­Work­Item?

Raymond Chen

Raymond

The Queue­User­Work­Item 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 Create­Threadpool­Work, but the old functions continue to work, for compatibility. (They are implemented as wrappers around the new thread pool functions.)

Curiously, the Queue­User­Work­Item function takes a LPTHREAD_START_ROUTINE function pointer as the function to run on a thread pool thread. This is curious because the LPTHREAD_START_ROUTINE function returns a DWORD. What does the system do with the DWORD returned by a work item?

Nothing.

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.)

8 comments

Comments are closed. Login to edit/delete your existing comments

  • Avatar
    Joshua Hudson

    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.

    • Emily Bowman
      Emily Bowman

      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.

        • Avatar
          Kasper Brandt

          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.

          • Avatar
            紅樓鍮

            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

            int f() {}

            the compiler may choose not to generate even a ret instruction (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:

            1. compile the void-returning function separately, and yet declare it in the consuming file as returning DWORD, and then take its address directly;
            2. compile QueueUserWorkItem separately (which it already has been), and yet declare QueueUserWorkItem as
              BOOL WINAPI QueueUserWorkItem(
                  void (*)(void *), // !!!
                  void *, DWORD)

              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.

          • Avatar
            Kasper Brandt

            > 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 6.3.2.3, 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.

          • Avatar
            紅樓鍮

            A pointer to a function of one type may be converted to a pointer to a function of another type and back again……

            which is not what we’re doing here.

            If a converted pointer is used to call a function whose type is not compatible with the pointed-to type……

            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.

          • Avatar
            Adam Rosenfield

            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).