What is the correct way of temporarily changing a thread’s preferred UI language?


A customer ran into a crashing bug in their shell extension. The shell extension wants to change the thread's preferred UI language temporarily, so that it can load its resources from a specific language. You'd think this would be easy:

// error checking elided for simplicity
// There is a bug in this code - read on

// Get the current thread preferred UI languages
ULONG cLanguages;
PZZWSTR pszzPrevLanguages;
ULONG cchPrevLanguages = 0;
GetThreadPreferredUILanguages(MUI_LANGUAGE_NAME,
                              &cLanguages, NULL,
                              &cchPrevLanguages);
pszzPrevLanguages = new WCHAR[cchPrevLanguages];
GetThreadPreferredUILanguages(MUI_LANGUAGE_NAME,
                              &cLanguages,
                              pszzPrevLanguages,
                              &cchPrevLanguages);

... change the thread preferred UI languages ...
... load resources ...

// Restore the original thread preferred UI languages
SetThreadPreferredUILanguages(MUI_LANGUAGE_NAME,
                              pszzPrevLanguages,
                              &cLanguages);
delete[] pszzPrevLanguages;

Approximately ten seconds after this code runs, Explorer crashes with the exception STATUS_CALLBACK_RETURNED_LANG whose description is "A threadpool worker thread enter a callback, which left with preferred languages set. This is unexpected, indicating that the callback missed clearing them." (Just before Explorer crashes, the message "ThreadPool: callback 77180274(05B67430) returned with preferred languages set" appears on the debugger, which says basically the same thing as the status code.)

Exercise: Why does it take ten seconds before the crash occurs?

This crash is puzzling, because it's claiming that the callback didn't reset the thread preferred languages, but you can see us doing it right there in the code when we call Set­Thread­Preferred­UI­Languages! Somebody's on crack, but who?

A closer reading of the error message indicates that the callback needs to "clear" the thread preferred languages, not merely reset them to their original values, and the documentation for Set­Thread­Preferred­UI­Languages says, "To clear the thread preferred UI languages list, the application can set this parameter to a null string or an empty double null-terminated string." Okay, so now the question is, "How can I tell, when I call Get­Thread­Preferred­UI­Languages, that the list of languages I receive back represents the clear state as opposed to indicating that some other code called Set­Thread­Preferred­UI­Languages before I did?"

The magic is the flag MUI_THREAD_LANGUAGES. If you pass this flag when you call Get­Thread­Preferred­UI­Languages, it will return a null string if the thread has not customized its preferred UI languages, indicating that the way to restore the thread's preferred UI language state is to clear it rather than setting it. Fortunately, this lines up nicely with the way you're supposed to clear the state, so at the end of the day there is no special case.

The fix to the above code, then, is to make the following simple change:

// error checking elided for simplicity

// Get the current thread preferred UI languages
ULONG cLanguages;
PZZWSTR pszzPrevLanguages;
ULONG cchPrevLanguages = 0;
GetThreadPreferredUILanguages(MUI_LANGUAGE_NAME |
                              MUI_THREAD_LANGUAGES,
                              &cLanguages, NULL,
                              &cchPrevLanguages);
pszzPrevLanguages = new WCHAR[cchPrevLanguages];
GetThreadPreferredUILanguages(MUI_LANGUAGE_NAME |
                              MUI_THREAD_LANGUAGES,
                              &cLanguages,
                              pszzPrevLanguages,
                              &cchPrevLanguages);

... change the thread preferred UI languages ...
... load resources ...

// Restore the original thread preferred UI languages
SetThreadPreferredUILanguages(MUI_LANGUAGE_NAME,
                              pszzPrevLanguages,
                              &cLanguages);
delete[] pszzPrevLanguages;

As you might expect, Michael Kaplan has his own thoughts on the Set­Thread­Preferred­UI­Languages function. In fact, he has several such thoughts.

Comments (10)
  1. Joshua says:

    Ouch. I'm glad I'm not the one who had to debug that.

  2. Maurits says:

    I suppose an ANSI double-null terminated string is a PZZASTR…

  3. Rick C says:

    "Why does it take ten seconds before the crash occurs?"

    Doesn't it take that long for a shell extension to be unloaded?

    [Please elaborate on the relationship between threads and DLLs in your world. -Raymond]
  4. Maurits says:

    Are the calls to GetThreadPreferredUILanguages necessary?  It seems that the "load resources from a specific language" scenario could be met by two calls to Set, and none to Get:

    SetThreadPreferredUILanguages(MUI_LANGUAGE_NAME, "foo-Bar", NULL);

    … load resources…

    SetThreadPreferredUILanguages(0, NULL, NULL);

    If you wanted to add a language to the end of the list, sure, you need GetThreadPreferredUILanguages.

    I believe the 10 second timeout is due to the threadpool thread waiting for new work to come along, before getting recycled.

  5. Joshua says:

    The answer is probably something along the lines of either that thread runs for 10 seconds after this bit of code or there is a thread pool with a 10 second timeout.

    [Hint: The error message says "threadpool". -Raymond]
  6. Drake Wilson says:

    This is interesting.  I just followed the link to your article on scalar versus vector delete in C++ (blogs.msdn.com/…/66660.aspx) on a whim to reread it.  One of your comments in response to Johan Ericsson refers to the existence of a followup article the next day.  But when I go to the archives page for February 2004 (blogs.msdn.com/…/02.aspx) which should correspond to the timestamp on the article, neither the original article nor any obvious followup appear on either of the two pages of the list that I see.

    I wonder whether this is another blog software glitch, or whether I've just done something stupid while navigating.

  7. laonianren says:

    @Drake Wilson:

    blogs.msdn.com/…/67384.aspx

    The blog archive is broken but search engines are mysteriously able to find the missing pages.

  8. Erzengel says:

    Maurits:

    Say, for example, you have the following:

    void InnerFunc()

    {

      var Original = GetThreadPreferredUILanguages();//Simplifying

      SetThreadPreferredUILanguages(WantedLanguage);

      //Do Stuff

      SetThreadPreferredUILanguages(Original);

    }

    void ThreadCallback1()

    {

      SetThreadPreferredUILanguages(SomeOtherLanguage);

      //DoStuff

      InnerFunc();

      //Do more stuff

      SetThreadPreferredUILanguages(null);

    }

    void ThreadCallback2()

    {

      InnerFunc();

    }

    In the case of ThreadCallback2, you'll want to "clear" your UI language. But in the case of ThreadCallback1, if you clear the UI language, you'll end up with "Do more stuff" using the wrong language. So yes, the Get is necessary. It's always good practice to leave things the way you found them.

  9. VSChawathe says:

    While administering my father's office PC, I've had my share of issues due to Internet dialers, productivity suites not working with Regional Settings option set to Indic scripts Marathi language i. e. Mumbai/Maharashtra's official language. Thnx for bringing awareness resource language bug. :)

  10. Maurits says:

    @Erzengel I see… that makes sense.

Comments are closed.