Why are HANDLE return values so inconsistent?


If you look at the various functions that return HANDLEs, you'll see that some of them return NULL (like CreateThread) and some of them return INVALID_HANDLE_VALUE (like CreateFile). You have to check the documentation to see what each particular function returns on failure.

Why are the return values so inconsistent?

The reasons, as you may suspect, are historical.

The values were chosen to be compatible with 16-bit Windows. The 16-bit functions OpenFile, _lopen and _lcreat return -1 on failure, so the 32-bit CreateFile function returns INVALID_HANDLE_VALUE in order to facilitate porting code from Win16.

(Armed with this, you can now answer the following trivia question: Why do I call CreateFile when I'm not actually creating a file? Shouldn't it be called OpenFile? Answer: Yes, OpenFile would have been a better name, but that name was already taken.)

On the other hand, there are no Win16 equivalents for CreateThread or CreateMutex, so they return NULL.

Since the precedent had now been set for inconsistent return values, whenever a new function got added, it was a bit of a toss-up whether the new function returned NULL or INVALID_HANDLE_VALUE.

This inconsistency has multiple consequences.

First, of course, you have to be careful to check the return values properly.

Second, it means that if you write a generic handle-wrapping class, you have to be mindful of two possible "not a handle" values.

Third, if you want to pre-initialize a HANDLE variable, you have to initialize it in a manner compatible with the function you intend to use. For example, the following code is wrong:

HANDLE h = NULL;
if (UseLogFile()) {
    h = CreateFile(...);
}
DoOtherStuff();
if (h) {
   Log(h);
}
DoOtherStuff();
if (h) {
    CloseHandle(h);
}

This code has two bugs. First, the return value from CreateFile is checked incorrectly. The code above checks for NULL instead of INVALID_HANDLE_VALUE. Second, the code initializes the h variable incorrectly. Here's the corrected version:

HANDLE h = INVALID_HANDLE_VALUE;
if (UseLogFile()) {
    h = CreateFile(...);
}
DoOtherStuff();
if (h != INVALID_HANDLE_VALUE) {
   Log(h);
}
DoOtherStuff();
if (h != INVALID_HANDLE_VALUE) {
    CloseHandle(h);
}

Fourth, you have to be particularly careful with the INVALID_HANDLE_VALUE value: By coincidence, the value INVALID_HANDLE_VALUE happens to be numerically equal to the pseudohandle returned by GetCurrentProcess(). Many kernel functions accept pseudohandles, so if if you mess up and accidentally call, say, WaitForSingleObject on a failed INVALID_HANDLE_VALUE handle, you will actually end up waiting on your own process. This wait will, of course, never complete, because a process is signalled when it exits, so you ended up waiting for yourself.

Comments (21)
  1. Anonymous says:

    I would have guessed it was called "CreateFile" instead of "OpenFile" because it creates a file handle or object.

  2. Anonymous says:

    My guess would have been because the same function can both create AND open files, thus settled for the CreateFile name.

  3. Anonymous says:

    (It’s also a parallel construction with the other handle-creation functions like CreateMutex, CreateEvent… But it’s still confusing.)

  4. Anonymous says:

    And it’ll stay this way forever, because Win64 needs to be compatible to Win32 needs to be compatible to Win16… You know, making all of this "compatibility" between the different versions of Win## just makes it harder and harder to master over the years.

  5. Anonymous says:

    And it’ll stay this way forever, because Win64 needs to be compatible to Win32 needs to

    > be compatible to Win16

    ad nauseum until Windows becomes such a kludge of hacks that it becomes impossible for Microsoft to maintain. (is this already happening?)

  6. Anonymous says:

    I don’t quite understand how CreateFile had to return -1 to "facilitate porting code from Win16". CreateFile didn’t exist in Win16.

  7. Anonymous says:

    In the hopes of convincing people to switch to Win32, there was a program called PORTTOOL.EXE that took a Win16 source program and ported it to Win32 (using various rewrite rules and heuristics). I suspect the return value allowed PORTTOOL.EXE to port various 16-bit file open calls to CreateFile without having to worry about trying to understand the error-checking logic.

  8. Anonymous says:

    Ahh, I see.

  9. Anonymous says:

    edmundo: it’s certainly possible to keep compatibility shims up in the top layers of the software stack, keeping the lower levels pretty clean and maintainable. If you’ve written your software well, with decent use of abstraction and information-hiding, and evaluate your maintenance fixes correctly, you can vary the implementation behind your interfaces without affecting the client of that interface.

    If the semantics of the interface remain the same, there’s no need to change it. On the contrary, change for change’s sake is almost never good in any software, least of all platform software. You leave yourself with no product to sell until you’re finished, and no ISV can sell their product until you’re finished either. Even then they can only sell to people who’ve already bought your new platform. I’m already finding myself wondering whether Longhorn will actually be released, or whether it will become another Cairo.

  10. Anonymous says:

    some of them return INVALID_HANDLE_VALUE

    > (like CreateFile).

    That is sometimes true and sometimes false, which is exactly why I asked about it when you coded an insufficient test. Under Windows 98, CreateFile sometimes returned INVALID_HANDLE_VALUE and sometimes returned NULL (and sometimes returned valid handles :-) I did not try to reproduce it under Windows XP, but continue to code defensively.

    if ((MyHandle == INVALID_HANDLE_VALUE) || (MyHandle == NULL)) …

  11. Anonymous says:

    Sorry for adding a second message in a row, but it’s not my fault no one else posted in between :-)

    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/sdkintro/sdkintro/strict_compliance.asp

    > General Requirements

    […]

    > You cannot use one handle type where another

    > is expected.

    […]

    > For best results, the generic HANDLE type

    > should be used only when necessary.

    I agree. But to do so for file handling, we have to call a 16-bit compatibility function. Why did Win32 break compatibility with this formerly respected practice?

  12. Anonymous says:

    ? You were never able to interchange file handles and other types of handles (like GDI handles). Any code that did was already broken.

  13. Anonymous says:

    To use an HFILE, one must call the 16-bit compatibility function OpenFile. Why does CreateFile return a generic HANDLE instead of an HFILE?

  14. Anonymous says:

    Because that’s what CloseHandle and ReadFile and WriteFile use. File handles are a special type of kernel handle, and kernel handles are manipulated as HANDLE.

    I don’t understand why you are calling 16-bit compatibility functions (that use HFILE – which if you look is just "int"). Use ReadFile and WriteFile.

    The purpose of that remark ("the generic HANDLE type should be used only when necessary") is to get people to stop using HANDLE for things that aren’t kernel handles (like GDI and USER handles).

  15. Anonymous says:

    I am not calling 16-bit compatibility functions for file access (except for application-specific .ini files as mentioned in the other thread). My question here was why Win32 took a backwards step from Win16. MSDN Platform SDK documentation recommends practices which Win16 got right in this case.

  16. Anonymous says:

    It was a backwards step in the sense that what used to be a separate type (HFILE) is now a generic HANDLE. (Of course, that separate type was just "int" so it wasn’t really a separate type.)

    It was a step forward in the uniform handling of kernel objects.

    Engineering is all about tradeoffs.

  17. Anonymous says:

    It isn’t a backwards step.

    One of the remarkable things about the Win32 API (which is generally far from remarkable) is that it’s quite polymorphic in this area. In general, a HANDLE is the same wherever it’s from.

    This is great. It means I don’t have to care if a HANDLE is a file, a thread, a process, a mutex, an event, a pipe, a directory, a physical disk, a volume, a mailslot, a console, a serial port, or a tape drive — I can do certain operations (such as close it or wait on it) in the same way.

    Obviously, not all combinations are meaningful; ReadFile doesn’t (at least, I assume it doesn’t, as it has no obvious behaviour) work with a mutex handle, for example. But it’s still pretty good — it’s more versatile than POSIX, for example.

  18. Anonymous says:

    3/31/2004 9:05 AM DrPizza:

    > It isn’t a backwards step.

    The I trust you’ll fix this MSDN page:

    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/sdkintro/sdkintro/strict_compliance.asp

  19. Anonymous says:

    Right, now that we’ve got the basics of reliability covered, we can move on to SafeHandle. If you’ve…

  20. Anonymous says:

    It’s a confusing story.

  21. Anonymous says:

    (Yes, Betas). While my previous post talked only about the past, this post talks about the present. Some of the…

Comments are closed.