The documentation for the GetÂFullÂPathÂName
function contains this dire warning:
Multithreaded applications and shared library code should not use the GetFullPathName function and should avoid using relative path names. The current directory state written by the SetCurrentDirectory function is stored as a global variable in each process, therefore multithreaded applications cannot reliably use this value without possible data corruption from other threads that may also be reading or setting this value. This limitation also applies to the SetCurrentDirectory and GetCurrentDirectory functions. The exception being when the application is guaranteed to be running in a single thread, for example parsing file names from the command line argument string in the main thread prior to creating any additional threads. Using relative path names in multithreaded applications or shared library code can yield unpredictable results and is not supported.
What is this warning trying to say? It seems to suggest that the current directory global variable is not thread-safe. Does this mean that all calls to SetÂCurrentÂDirectory
and GetÂCurrentÂDirectory
need to be serialized by the application?
No, that’s not what it’s saying.
What it’s trying to say is that the meaning of a relative path depends on the current value of the current directory. The value of the current directory can be changed by any thread at any time, so make sure that you understand that the result of the GetÂFullÂPathÂName
function is a “moment in time” calculation. Resolving the same relative path in consecutive calls to the GetÂFullÂPathÂName
function could result in different results if the current directory changed in between.
If you find yourself with a relative path, you have a few choices.
One option is to pass it as a relative path to a function like CreateÂFile
, but only once, and as soon as possible. Don’t assume that a second CreateÂFile
will open the same file. You want to use it as soon as possible because the user entered the relative path based on some underlying assumptions about the current directory, and the longer you wait, the more likely those assumptions are going to be wrong.
Another option is to convert it to an absolute path as soon as possible, and use the absolute path (at your leisure) from then on. Again, you want to convert it as soon as possible to reduce the likelihood of changes to the conditions under which the user entered the relative path.
If the relative path didn’t come from the user but rather from, say, a configuration file or another process, then things are kind of sketchy. The relative path in the configuration file is probably intended to be interpreted relative to some anchor point (such as the configuration file itself), not relative to something as fickle as the process’s current directory. And a relative path received from another process is even more sketchy, because that other process has no idea what your current directory is.
The concept of the current directory was inherited from MS-DOS, which in turn got it from the concept of the current drive in CP/M (and probably influenced by a similar concept in Unix). CP/M was a single-threaded operating system, so there were no race conditions related to the current directory. And at the time, Unix supported only one thread per process, so the issue never arose there either.
The idea of a current directory in today’s multithreaded world is a bit of an anachronism, as if there’s only one “place” a process can be at a time. Standard handles were also designed in a single-threaded world. I think the convention for the current directory should be that only the main thread of the main process can change the current directory: Background threads and helper libraries should keep their hands off.
Bonus chatter: Don’t forget to pass the OFN_
flag when you use the common file dialogs, to tell them not to change the current directory.
Bonus reading: The curse of the current directory.
I’m curious — the environment variable block has a number of pseudovariables that save per-drive paths for some reason — what are these used for, if not current directory?
Examples:
=C:=C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\Common7\\IDE
=E:=E:\\My Projects
As noted, they are for backward compatibility, so that Windows can pretend that each drive has a current directory (even though it doesn’t). Environment variables are process-global and are therefore subject to the same race conditions as the current directory.
Glibc added per-thread current directory awhile ago for anybody who wants to use it. In the Windows world, the thing is available if you're willing to dig out the Nt Native APIs but Win32 has no such concept. Too bad.
In any case, both my GUI applications and my services set their current directory to be the application directory at startup; leaving only command line programs to not do so, in which case they never set...
NT APIs can set per-thread current directory? Would you know which one?
OBJECT_ATTRIBUTES structure; but it will not back-propagate to Win32 so don’t get your hopes up.
Also, the open and save common dialogs change the current directory to the one where the selected file is, at least in classic Visual Basic.
In the beginning, I used to reset the current directory to the executable directory after each call. But then I evolved to do what Raymond recommends, and started passing only full paths to APIs and file calls. Every file path is parsed by a function called UnpackFilename(), which extends a relative...
Ah, happy memories of the time a printer driver decided to change the current directory out from under me…
GetOpenFilename is documented to ignore the NOCHANGEDIR flag and might change the current directory.
I am one more fault from a bad explorer extension away from calling GetOpenFileName out-of-process anyway.