Why does FindExecutable behave erratically for files with extensions longer than three characters? (And what can you do about it?)


The Find­Executable function looks up the executable responsible for launching a particular file. This is a dubious undertaking, because it assumes that the thing that launches a file is an executable. There are other things capable of launching a file, such as a DDE command, a context menu shell extension, or a custom drop target. What should Find­Executable return in those cases?

Okay, so if Find­Executable is based upon a flawed assumption, why does it even exist?

Because at the time it was originally introduced, the assumption was valid.

The Find­Executable function comes from 16-bit Windows, and back in those days, there were no context menu shell extensions or custom drop targets. (There was DDE, but that's okay, because programs still have to register an executable to be used in the fallback case when nobody responds to the DDE message.)

In the port to 32-bit Windows, the Find­Executable function remains, but it works only in the case where files were registered in the 16-bit way; that is, with a command line executable. It so happens that most file types are still registered that way, so the Find­Executable function basically still works.

Since the Find­Executable function is basically a throwback to 16-bit Windows, there is another attempt to accommodate the 16-bit world that is not as obvious: The Find­Executable function takes the thing you pass and converts it into a short file name before trying to look up the handler.

The effect of the conversion to a short file name depends on a bunch of things.

If the volume does not have short file name autogeneration enabled, then the conversion to a short file name has no effect. But if the volume does have short file name autogeneration enabled, then the net effect is that the extension gets truncated to three characters. foo.abcde becomes foo~1.abc. And then Find­Executable looks up and returns the handler for the .abc extension instead of the .abcde extension.

Back in the days before long file names, all file extensions were truncated to 3 characters. if you asked for foo.abcde, you got foo.abc. The Find­Executable function tries to maintain this compatibility with older applications. Newer applications shouldn't be using Find­Executable anyway, seeing as the handler for a file type may not even be an executable.

I accept that the concept of finding the executable associated with a file is flawed in the face of handlers that do not take the form of an executable, but I still want to get the executable associated with a file, if possible, with the understanding that the answer may be incorrect.

You can use the Assoc­Query­String function to get the executable associated with the default verb of a file extension, if one exists.

HRESULT FindExecutableAssociatedWithFileExtension(
    _In_ PCWSTR extension,
    _Out_ PWSTR resultBuffer,
    _In_ DWORD bufferLength)
{
 return AssocQueryString(ASSOCF_INIT_INGORENUNKNOWN,
                         ASSOCSTR_EXECUTABLE,
                         fullPath,
                         nullptr,
                         resultBuffer,
                         &bufferLength);
}

The ASSOCF_INIT_UNKNOWN flag says that if the file extension has no handler, don't return the "Open unknown file" handler.

This is not exactly the same as Find­Executable because that function has special-case code for when you pass in, for example, excel.exe. In those cases, the Find­Executable function just returns the file itself, since executables are their own handlers.

The ASSOCF_INIT_UNKNOWN flag was added in Windows 7. What do you do for older versions of Windows? Well, you're in luck. Older versions of Windows didn't have the "Open unknown file" handler, so if there is no registered handler, the call will simply fail. (Indeed, the introduction of the "Open unknown file" handler is what most likely prompted the creation of the ASSOCF_INIT_UNKNOWN flag in the first place.) As a second mark of good fortune, the flag is ignored by older versions of Windows, so you can go ahead and pass the flag unconditionally: On versions of Windows that support it, it does what you want. And on versions of Windows that don't support it, they already behave the way you want by default.

Comments (16)
  1. Karellen says:

    “As a second mark of good fortune, the flag is ignored by older versions of Windows, so you can go ahead and pass the flag unconditionally”

    It’s good fortune in this case, and certainly used to be common everywhere, but is the behaviour of ignoring unknown flags recommended/allowed for new APIs?

    I know that some library vendors have had issues where they’ve ignored unknown flags before, and users of the library have ended up passing in garbage values, because that worked. However, when new flags are defined for future API-/ABI-compatible library updates, those users end up enabling them, and getting behaviour they didn’t actually want.

    Conversely, if code using the new API flags ends up getting (dynamically?) linked against an old version of the library, where the library *doesn’t* know about those flags – some of which might enable security features the client is relying on – the client also doesn’t get the behaviour they asked for, but silently. They’d rather get a “not supported” error in that case.

    It’s another one of those times when Postel’s Law, which was great for helping get new technologies off the ground, ends up causing unintended problems further down the line. See also: tag soup.

    1. The AssocQueryString function did the wrong thing (accepting invalid flags), but we got lucky and it happens to work out in our favor. Most of the time, these mistakes lead to sadness.

      1. Karellen says:

        Um, yeah. That’s why I was asking if this behaviour is still recommended/allowed for new APIs.

        ?

      2. Ivo says:

        So in a world where you can’t pass invalid flags to functions, and where you can’t reliably get the OS version, what is the recommended way to write code that targets different versions?

        1. You declare that your app has been tested with Windows XP, Windows Vista, and Windows 7. Then you ask what version you are running on. If running on Windows 7 or higher, you will be told Windows 7. At which point you know that the UNKNOWN flag is supported.

          1. RKZENITH says:

            Who do we ask about the version? The user? Because, as Ivo says, we’re repeatedly told not to ask Windows.

            Specifically, from the MSDN page for VerifyVersionInfo:
            “Identifying the current operating system is usually not the best way to determine whether a particular operating system feature is present. This is because the operating system may have had new features added in a redistributable DLL.”

            And the MSDN page for GetVersionEx:
            “With the release of Windows 8.1, the behavior of the GetVersionEx API has changed in the value it will return for the operating system version. The value returned by the GetVersionEx function now depends on how the application is manifested. Applications not manifested for Windows 8.1 or Windows 10 will return the Windows 8 OS version value (6.2). Once an application is manifested for a given operating system version, GetVersionEx will always return the version that the application is manifested for in future releases.”

            The way that I read those two pages, starting with 8.1, Windows will return the version that you tell it to return, not the version that’s actually running, completely defeating the point of doing a version check in the first place.

          2. Anon says:

            Apparently, I can’t reply to RKZENITH. This is a stupid threading model. Anyway…

            The point of doing a version check is to verify the behaviour of the OS. The manifest says “I have verified my application with the behaviour of .”
            If you’re using APIs that only exist in Windows 8 and are manifested for Vista/7/8, and you get back “Win7”, you know not to use those APIs.

            If you’re depending on behaviour that existed in Win7 and you get back “Win7,” you know you’re at least running in a compatibility layer for Win7.

            If you’re depending on bugs that existed in Win7, you should stop because that’s stupid.

            If you want to know if you’re on a newer OS version than you’ve tested on, you should stop. There’s no valid reason to do that. If you *are* doing it, you’re probably trying to annoy the user by telling them that your application doesn’t work on their OS, despite it probably working perfectly fine.

  2. Stefan Kanthak says:

    | Older versions of Windows didn’t have the “Open unknown file” handler

    Of course they have: see HKCR\unknown\shell\openas, introduced with Windows 95

  3. RP says:

    “This is not exactly the same as Find­Executable because that function has special-case code for when you pass in, for example, excel.exe. In those cases, the Find­Executable function just returns the file itself, since executables are their own handlers.”

    I find that kind of surprising. Excel.exe isn’t really its own handler, since you don’t get useful results if you run excel.exe excel.exe (i.e. if you pass excel.exe as a parameter to excel.exe).

    If the .exe really must return a handler (as GetExecutable decided to), I’d expect the handler for excel.exe to be something more like cmd.exe (since you can do cmd.exe /cexcel.exe) or explorer.exe (since explorer.exe excel.exe seems to work) (obviously I know in 16-bit Windows it would have been command.com and progman.exe – I haven’t got a 16-bit installation handy to see what happens when these are passed exes as parameters).

  4. Glassware says:

    I’m amazed to discover that DDE still exists.

    I wrote a DDE library for Windows 3.0 so that I could connect to a program that shared its log data via DDE; and I remember at the time thinking that DDE really seemed quite half-baked. Does it still function today? Is it still technically supported or has it been fully replaced by OLE Automation?

    I am fascinated however with the interesting collection of metaphors in windows. Originally, all communication was done via messages that would get sent to your message loop. Nowadays you have a ton of different methods of getting callback via remote procedure invocation.

    1. Yuhong Bao says:

      DDE is still supported. Win32 even has special support for the DDE messages in addition to DDEML. It is probably better to use the messages directly so that for example timeouts on DDE broadcasts are possible.

    2. Marcel says:

      I have written DDE code only once 15 years ago or so, but it’s still in use today: jumping to a specific target in a PDF using Adobe Reader for our internal help feature.

  5. Henri Hein says:

    The code snip says ASSOCF_INIT_INGORENUNKNOWN. The comments say ASSOCF_INIT_UNKNOWN. I think in both cases you mean ASSOCF_INIT_IGNOREUNKNOWN. It’s the only ASSOCF flag with both INIT and UNKNOWN in it, that I could find.

    1. Scarlet Manuka says:

      I was wondering about that. Also about why we might wish to ignore known nuns.

      1. DWalker says:

        You should NEVER ignore known nuns! Didn’t you see the movie The Blues Brothers?

    2. cheong00 says:

      It seems to me that the description of this flag on MSDN is somewhat problematic too.

      “Introduced in Windows 7. Specifies that the “Unknown” ProgID should be ignored; instead, fail.”

      Sounds to me that if “unknown” and “known” handler both exist, it’ll still return error (i.e.: fail).

Comments are closed.

Skip to main content