The Locale Explorer: GetNLSVersion

Home
Back To Tips Page

Back to LocaleExplorer

The GetNLSVersion API "returns information about the current version of a specifed NLS capability".  The problem is that this API is not supported in versions of Windows less than Server 2003.  This presents certain challenges if a program is going to be run on a lower version of Windows.

This screen snapshot shows the execution of GetNLSVersion running on Windows Server 2003.

There is currently only one NLS_FUNCTION supported, COMPARE_STRING.  Presumably in later versions of Windows, perhaps even in Vista, there may be more options here.  The contents of the NLSVERSIONINFO structure are displayed.  If an error occurs, this latter information is suppressed.

This screen snapshot shows GetNLSVersion executed on Windows XP Professional SP2.  Note that since this is not a version of the operating system that supports this API, an error is displayed.

This leads to a common problem when trying to implement a piece of code which would like to take advantage of a newer version of Windows but which must run on older versions.

Aside: Conditional compilation in windows.h

In some cases, you will get an error message in compilation because an older Platform SDK is being used for the compilation.  In such cases you will get an error message, such as a symbol not found during compilation.  You might find the definition in the header file defined something like

#if (WINVER >= 0x0501

which will then define APIs for Windows version 5.01, that is, Windows XP.  Strangely enough, GetNLSVersion is not under control of one of these conditionals!  So it will compile successfully even in Windows XP.

One of the questions you might wonder about is what WINVER  is actually set to.  This takes a little bit of trickery.  For example, adding the following lines will print out the value of WINVER:

#define makestring2(x) #x
#define makestring(x) makestring2(x)
#pragma message(__FILE__ "(" makestring(__LINE__) ") Current version is " makestring(WINVER) "\n")

This will generate, during compilation, the nessage

i:\tests\getnlsinfoexample\getnlsinfoexample\getnlsinfoexample.cpp(7) Current version is 0x0501

Note the trick of needing two levels of macros to print the expansion of another macro.  I apply this technique to expand both __LINE__ and WINVER.

If you need to change WINVER, you must specify the value before the #include of windows.h, in stdafx.h:

#define WINVER 0x0502
#include <windows.h>

Because of the way precompiled headers work, this is the only place it can be defined.  The following code will not work:

#define WINVER 0x0502
#include "stdafx.h"

This is because everything that precedes the #include of stdafx.h is treated as commentary by the compiler.

Running APIs on an older version of Windows

Even if your code compiles, it will not run on older version of Windows.  Either it compiles because, as in the case of GetNLSVersion, the header files erroneously include it, or, as described above, you have explicitly set the WINVER so the definition can be found.  While an explicit call on GetNLSVersion will compile, and link, but an attempt to run it will cause this box to pop up:

This is because the executable contains an "import record" for KERNEL32!GetNLSVersion, but running on versions of the system less than Server 2003 means that the import record cannot be resolved.  The consequence of this is that the program cannot be run.

The solution here is to defer the binding, which is what I do.  There is a checkbox, Create backward-compatible code, which will choose a template as illustrated below.

NLSVERSIONINFO info = {sizeof(NLSVERSIONINFO) }; 
LCID lcid = LOCALE_SYSTEM_DEFAULT;
   { /* GetNLSVersion */
    typedef BOOL (WINAPI * GetNLSVersionProc)(NLS_FUNCTION function, LCID locale, LPNLSVERSIONINFO info); // [1] 
    GetNLSVersionProc GetNLSVersion; // [2] 

    HMODULE kernel32 = ::GetModuleHandle(_T("KERNEL32")); // [3]
    GetNLSVersion = (GetNLSVersionProc)::GetProcAddress(kernel32, "GetNLSVersion"); // note 8-bit name, not _T()! // [4] 
    if(GetNLSVersion == NULL || !GetNLSVersion(COMPARE_STRING, lcid, &info))  // [5]
       { /* failed */
        DWORD err = ::GetLastError();
        // .. deal with error here
       } /* failed */
   } /* GetNLSVersion */

This code illustrates several key ideas of building backward-compatible API calls.

You can do this without the typedef, but you will find the code a lot easier to write if you create the typedef cased on the function prototype. Unfortunately, the API definitions given in the documentation are usually misleading, because the explicit calling convention, such as WINAPI is not defined in the documentation.  When necessary, you can verify this against the appropriate Windows header file.

In the Locale Explorer, the code that is used is quite similar to this code.  The error handler code causes the state to be set that sets the error message in an otherwise hidden static control, and disables the other controls.

ANSI vs. Unicode apps

Generally, you need to worry about the ANSI vs. Unicode API distinctions.  The above code works because GetNLSVersion does not take string arguments, and therefore does not have -A and -W suffices.  But supposed you wanted to use this technique for an API that did use string arguments.  Then the above solution would not work.  To illustrate this, I will show it with a well-defined (and ancient) API, AddAtom.  There is no need to do this with such a common API, but it illustrates the point well.

First, it is important to note that there is no such thing as the AddAtom API.  The name AddAtom is a macro.  The real API entry points are AddAtomA and AddAtomW. The following excerpt from the file winbase.h summarizes the definitions found.

ATOM WINAPI AddAtomA(IN LPCSTR lpString);
ATOM WINAPI AddAtomW(IN LPCWSTR lpString);
#ifdef UNICODE
#define AddAtom AddAtomW
#else
#define AddAtom AddAtomA
#endif // !UNICODE

Therefore, you can't call GetProcAddress on the string "AddAtom" because this will generate an error return.  The API entry point does not exist.

ASSERT(::GetProcAddress(kernel32, "AddAtom") == NULL);

will never fail.

Of course, you could do something like

#ifdef _UNICODE
    AddAtom = ::GetProcAddress(kernel32, "AddAtomW");
#else
   AddAtom = ::GetProcAddress(kernel32, "AddAtomA");
#endif

but that is a bit of a pain to use everywhere.  But look at the makestring macro definitions.  We can rewrite the code rather simply as

void AddAtomTest()
   {
    HMODULE kernel32 = ::GetModuleHandle(_T("KERNEL32"));
    typedef (WINAPI * AddAtomProc)(LPCTSTR s);
    AddAtomProc AddAtom = (AddAtomProc)GetProcAddress(kernel32, makestring(AddAtom));
    ATOM a = AddAtom(_T("test"));
   }

By adding the /P switch (it is case-sensitive, and different from /p, please note), we can recompile and we will get a .i file, which represents the preprocessed input.  To do this, select the project properties, then in the C/C++ options, select the Command Line option, and under Additional Options add the /P option as shown below.

Looking at the file compiled as an ANSI app, we see the above code compiles as

void AddAtomTest()
   {
    HMODULE module = ::GetModuleHandleA("KERNEL32");
    typedef (__stdcall * AddAtomProc)(LPCTSTR s);
    AddAtomProc AddAtomA = (AddAtomProc)GetProcAddress(module, "AddAtomA");
    ATOM a = AddAtomA("test");
   }

and if we compile with _UNICODE/UNICODE defined, we see that it compiles as

void AddAtomTest()
   {
    HMODULE module = ::GetModuleHandleW(L"KERNEL32");
    typedef (__stdcall * AddAtomProc)(LPCTSTR s);
    AddAtomProc AddAtomW = (AddAtomProc)GetProcAddress(module, "AddAtomW");
    ATOM a = AddAtomW(L"test");
   }

Creating a Unicode configuration

To get the Unicode build, in VS.NET you can bring up the Build > Configuration Manager.  Create a new configuration.

Having selected the <New...> option, the following dialog comes up.  Fill in the name of the new configuration, and the configuration to base it on.

Then select the project properties, and in the C/C++ options select the Preprocessor option.  Add the _UNICODE and UNICODE options separated by semicolons as shown.

 

[Dividing Line Image]

The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.

Send mail to newcomer@flounder.com with questions or comments about this web site.
Copyright 2005-2006 Joseph M. Newcomer/FlounderCraft Ltd.  All Rights Reserved.
Last modified: May 14, 2011