I would like my VB 6 application to detect and display the version of Windows that is running on.
I have tried this code from another Stack Overflow question, but it does not work for me. It displays the correct version number on older versions of Windows (like Windows XP and Vista), but it cannot detect Windows 10. For some reason, it says that Windows 10 is Windows 8.
I thought Windows 10 would have a major version of "10" and a minor version of "0", and this chart of Windows version numbers confirms that it does. Why, then, does the GetVersionEx function never actually return version 10.0?
How can I accurately distinguish between Windows 8, Windows 8.1, and Windows 10?
Why is the old code broken?
The code in that other answer works well for older versions of Windows. Specifically, it handles all the way up to Windows 8 (version 6.2) without a hitch. But as you've noticed, things start to go wrong on Windows 8.1 (version 6.3) and Windows 10 (version 10.0). The code looks like it should work, but it's getting version 6.2 for any version after Windows 8.
The reason for this is that Microsoft has decided to change how Windows reports its version number to applications. In an attempt to prevent old programs from erroneously deciding not to run on these latest versions of Windows, the operating system has "peaked out" its version number at 6.2. While Windows 8.1 and 10 still have internal version numbers of 6.3 and 10.0, respectively, they continue to report their version number as 6.2 to older applications. The idea is, essentially, "you cannot handle the truth", so it will be withheld from you. Under the hood, there are compatibility shims between your application and the system that are responsible for faking the version number whenever you call these API functions.
These particular compatibility shims were first introduced in Windows 8.1, and affected several of the version information retrieval APIs. In Windows 10, the compatibility shims begin to affect nearly all of the ways that a version number can be retrieved, including attempts to read the version number directly from system files.
In fact, these old version information retrieval APIs (like the GetVersionEx function used by that other answer) have been officially "deprecated" by Microsoft. In new code, you are supposed to use the Version Helper functions to determine the underlying version of Windows. But there are two problems with these functions:
There are a whole bunch of them—one to detect every version of Windows, including "point" versions—and they are not exported from any system DLL. Rather, they are inline functions defined in a C/C++ header file distributed with the Windows SDK. This works great for C and C++ programmers, but what is a humble VB 6 programmer to do? You can't call any of these "helper" functions from VB 6.
Even if you could call them from VB 6, Windows 10 extended the reach of the compatibility shims (as I mentioned above), so that even the IsWindows8Point1OrGreater and IsWindows10OrGreater functions will lie to you.
A Compatibility Manifest
The ideal solution, and the one that the linked SDK documentation alludes to, is to embed a manifest in your application's EXE with compatibility information. Manifest files were first introduced in Windows XP as a way of bundling metadata with an application, and the amount of information that can be included in a manifest file has increased with each new version of Windows.
The relevant portion of the manifest file is a section called compatibility. It might look something like this (a manifest is just an XML file that adheres to a specific format):
<!-- Declare support for various versions of Windows -->
<ms_compatibility:compatibility xmlns:ms_compatibility="urn:schemas-microsoft-com:compatibility.v1" xmlns="urn:schemas-microsoft-com:compatibility.v1">
<ms_compatibility:application>
<!-- Windows Vista/Server 2008 -->
<ms_compatibility:supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
<!-- Windows 7/Server 2008 R2 -->
<ms_compatibility:supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows 8/Server 2012 -->
<ms_compatibility:supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 8.1/Server 2012 R2 -->
<ms_compatibility:supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 10 -->
<ms_compatibility:supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</ms_compatibility:application>
</ms_compatibility:compatibility>
The way it works is each version of Windows (since Vista) has a GUID, and if your manifest includes that GUID as a supportedOS, then the system knows that you wrote the application after that version was released. It is therefore assumed that you are prepared to deal with its breaking changes and new features, so the compatibility shims are not applied to your application. Including, of course, the GetVersionEx function that is used by the original code.
Chances are, if you are a conscientious Windows developer, you are already embedding a manifest in your VB 6 app. You need a manifest to get themed controls (by explicitly opting-in to version 6 of ComCtl32.dll), to prevent UAC virtualization (by requesting only asInvoker privileges), and perhaps even to prevent DPI virtualization (by marking yourself as high-DPI aware). You can find lots of information online about how these and other settings in application manifests work.
If you are already embedding a manifest file in your app, then it is a simple matter of adding the Windows 8.1 and Windows 10 GUIDs to your existing manifest. This will cut through the OS-version lies.
If you are not already embedding a manifest file, then you have some work ahead of you. VB 6 was released several years before manifests had been conceived, and as such, the IDE does not have any built-in facility to deal with them. You have to deal with them yourself. See here for tips on embedding a manifest file in VB 6. The long and short is that they are just text files, so you can create one in Notepad and embed it into your EXE with mt.exe (part of the Windows SDK). There are various possibilities for automating this process, or you can do it manually after completing a build.
An Alternative Solution
If you don't want to fuss with a manifest, there is another solution. It involves only adding code to your VB 6 project and does not need a manifest of any kind to work.
There is another little-known API function that you can call to retrieve the true OS version. It is actually the internal kernel-mode function that the GetVersionEx and VerifyVersionInfo functions call down to. But when you call it directly, you avoid the compatibility shims that would normally be applied, which means that you get the real, unfiltered version information.
This function is called RtlGetVersion, and as the linked documentation suggests, it is a run-time routine intended for use by drivers. But thanks to the magic of VB 6's ability to dynamically call native API functions, we can use it from our application. The following module shows how it might be used:
'==================================================================================
' RealWinVer.bas by Cody Gray, 2016
'
' (Freely available for use and modification, provided that credit is given to the
' original author. Including a comment in the code with my name and/or a link to
' this Stack Overflow answer is sufficient.)
'==================================================================================
Option Explicit
''''''''''''''''''''''''''''''''''''''''''''''''''
' Windows SDK Constants, Types, & Functions
''''''''''''''''''''''''''''''''''''''''''''''''''
Private Const cbCSDVersion As Long = 128 * 2
Private Const STATUS_SUCCESS As Long = 0
Private Const VER_PLATFORM_WIN32s As Long = 0
Private Const VER_PLATFORM_WIN32_WINDOWS As Long = 1
Private Const VER_PLATFORM_WIN32_NT As Long = 2
Private Const VER_NT_WORKSTATION As Byte = 1
Private Const VER_NT_DOMAIN_CONTROLLER As Byte = 2
Private Const VER_NT_SERVER As Byte = 3
Private Const VER_SUITE_PERSONAL As Integer = &H200
Private Type RTL_OSVERSIONINFOEXW
dwOSVersionInfoSize As Long
dwMajorVersion As Long
dwMinorVersion As Long
dwBuildNumber As Long
dwPlatformId As Long
szCSDVersion As String * cbCSDVersion
wServicePackMajor As Integer
wServicePackMinor As Integer
wSuiteMask As Integer
wProductType As Byte
wReserved As Byte
End Type
Private Declare Function RtlGetVersion Lib "ntdll" _
(lpVersionInformation As RTL_OSVERSIONINFOEXW) As Long
''''''''''''''''''''''''''''''''''''''''''''''''''
' Internal Helper Functions
''''''''''''''''''''''''''''''''''''''''''''''''''
Private Function IsWinServerVersion(ByRef ver As RTL_OSVERSIONINFOEXW) As Boolean
' There are three documented values for "wProductType".
' Two of the values mean that the OS is a server versions,
' while the other value signifies a home/workstation version.
Debug.Assert ver.wProductType = VER_NT_WORKSTATION Or _
ver.wProductType = VER_NT_DOMAIN_CONTROLLER Or _
ver.wProductType = VER_NT_SERVER
IsWinServerVersion = (ver.wProductType <> VER_NT_WORKSTATION)
End Function
Private Function GetWinVerNumber(ByRef ver As RTL_OSVERSIONINFOEXW) As String
Debug.Assert ver.dwPlatformId = VER_PLATFORM_WIN32_NT
GetWinVerNumber = ver.dwMajorVersion & "." & _
ver.dwMinorVersion & "." & _
ver.dwBuildNumber
End Function
Private Function GetWinSPVerNumber(ByRef ver As RTL_OSVERSIONINFOEXW) As String
Debug.Assert ver.dwPlatformId = VER_PLATFORM_WIN32_NT
If (ver.wServicePackMajor > 0) Then
If (ver.wServicePackMinor > 0) Then
GetWinSPVerNumber = "SP" & CStr(ver.wServicePackMajor) & "." & CStr(ver.wServicePackMinor)
Exit Function
Else
GetWinSPVerNumber = "SP" & CStr(ver.wServicePackMajor)
Exit Function
End If
End If
End Function
Private Function GetWinVerName(ByRef ver As RTL_OSVERSIONINFOEXW) As String
Debug.Assert ver.dwPlatformId = VER_PLATFORM_WIN32_NT
Select Case ver.dwMajorVersion
Case 3
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows NT 3.5 Server"
Exit Function
Else
GetWinVerName = "Windows NT 3.5 Workstation"
Exit Function
End If
Case 4
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows NT 4.0 Server"
Exit Function
Else
GetWinVerName = "Windows NT 4.0 Workstation"
Exit Function
End If
Case 5
Select Case ver.dwMinorVersion
Case 0
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows 2000 Server"
Exit Function
Else
GetWinVerName = "Windows 2000 Workstation"
Exit Function
End If
Case 1
If (ver.wSuiteMask And VER_SUITE_PERSONAL) Then
GetWinVerName = "Windows XP Home Edition"
Exit Function
Else
GetWinVerName = "Windows XP Professional"
Exit Function
End If
Case 2
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2003"
Exit Function
Else
GetWinVerName = "Windows XP 64-bit Edition"
Exit Function
End If
Case Else
Debug.Assert False
End Select
Case 6
Select Case ver.dwMinorVersion
Case 0
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2008"
Exit Function
Else
GetWinVerName = "Windows Vista"
Exit Function
End If
Case 1
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2008 R2"
Exit Function
Else
GetWinVerName = "Windows 7"
Exit Function
End If
Case 2
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2012"
Exit Function
Else
GetWinVerName = "Windows 8"
Exit Function
End If
Case 3
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2012 R2"
Exit Function
Else
GetWinVerName = "Windows 8.1"
Exit Function
End If
Case Else
Debug.Assert False
End Select
Case 10
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2016"
Exit Function
Else
GetWinVerName = "Windows 10"
Exit Function
End If
Case Else
Debug.Assert False
End Select
GetWinVerName = "Unrecognized Version"
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''
' Public Functions
''''''''''''''''''''''''''''''''''''''''''''''''''
' Returns a string that contains the name of the underlying version of Windows,
' the major version of the most recently installed service pack, and the actual
' version number (in "Major.Minor.Build" format).
'
' For example: "Windows Server 2003 SP2 (v5.2.3790)" or
' "Windows 10 (v10.0.14342)"
'
' This function returns the *real* Windows version, and works correctly on all
' operating systems, including Windows 10, regardless of whether or not the
' application includes a manifest. It calls the native NT version-info function
' directly in order to bypass compatibility shims that would otherwise lie to
' you about the real version number.
Public Function GetActualWindowsVersion() As String
Dim ver As RTL_OSVERSIONINFOEXW
ver.dwOSVersionInfoSize = Len(ver)
If (RtlGetVersion(ver) <> STATUS_SUCCESS) Then
GetActualWindowsVersion = "Failed to retrieve Windows version"
End If
' The following version-parsing logic assumes that the operating system
' is some version of Windows NT. This assumption will be true if you
' are running any version of Windows released in the past 15 years,
' including several that were released before that.
Debug.Assert ver.dwPlatformId = VER_PLATFORM_WIN32_NT
GetActualWindowsVersion = GetWinVerName(ver) & " " & GetWinSPVerNumber(ver) & _
" (v" & GetWinVerNumber(ver) & ")"
End Function
The intended public interface is a single function called GetActualWindowsVersion, which returns a string containing the name of the actual underlying version of Windows. For example, it might return "Windows Server 2003 SP2 (v5.2.3790)" or "Windows 10 (v10.0.14342)".
(Fully tested and working on Windows 10!)
The module's public function calls a couple of internal helper functions that parse information out of the native RTL_OSVERSIONINFOEXW data structure, simplifying the code slightly. There is even more information available in this structure if you want to take the time to modify the code to extract it. For example, there is a wSuiteMask member that contains flags, whose presence indicate certain features or product types. An example of how this information might be used appears in the GetWinVerName helper function, where the VER_SUITE_PERSONAL flag is checked to see if it is Windows XP Home or Pro.
Final Thoughts
There are several other "solutions" to this problem floating around online. I recommend avoiding these.
One popular suggestion is to try and read the version number from the Registry. This is a terrible idea. The Registry is neither intended as nor documented as a public interface for programs. This means such code is relying on implementation details that are subject to change at any time, leaving you back in a situation of breakage—the very problem we are trying to solve in the first place! There is never an advantage in querying the Registry over calling a documented API function.
Another frequently suggested option is to use WMI to retrieve the OS version information. This is a better idea than the Registry, since it is actually a documented, public interface, but it is still not an ideal solution. For one thing, WMI is a very heavy dependency. Not all systems will have WMI running, so you will need to ensure that it is enabled, or your code will not work. And if this is the only thing you need to use WMI for, it will be very slow because you have to wait for WMI to get up and running first. Furthermore, querying WMI programmatically from VB 6 is difficult. We don't have it as easy as those PowerShell folks! However, if you are using WMI anyway, it would be a handy way to get a human-readable OS version string. You can do this by querying Win32_OperatingSystem.Name.
I've even seen other hacks like reading the version from the process's PEB block! Granted, that is for Delphi, not VB 6, and since there is no inline assembly in VB 6, I'm not even sure if you could come up with a VB 6 equivalent. But even in Delphi, this is a very bad idea because it too relies on implementation details. Just…don't.
As an adjunct to the above manifest solution for GetVersionEx, place the following after the case 6 block for osv.dwVerMajor in Cody's code:
Case 10 'Note: The following works only with updated manifest
Select Case osv.dwVerMinor
Case 0
GetWindowsVersion = "Windows 10/Server 2016"
Case Else
End Select
The word from MSDN: "GetVersionEx may be altered or unavailable for releases after Windows 8.1." is something to watch, however.
To add to Cody's answer: remember, if running from the VB 6 IDE, it will report the compatibility you selected to get VB 6 to run, for example, on Windows 11 from the IDE, it reports:
Windows XP Home Edition SP2 (V5.1.2600)
If I compile and run the executable on the same Windows 11 machine, it reports:
Windows 10 (v10.0.2200)
I'll try to be brief: We have code written in Visual Basic 6.0 that I am trying to compile on a Windows 7 64-bit computer. (Previously this was compiled on an old XP computer.) Most of the code seems to compile correctly, however certain code that has Attributes, like the NewEnum, are not honoring the VB attribute. Here is an example:
The VB6 method looks like this:
Public Property Get NewEnum() As IUnknown
Attribute NewEnum.VB_UserMemId = -4
Attribute NewEnum.VB_MemberFlags = "40"
'this property allows you to enumerate
'this collection with the For...Each syntax
Set NewEnum = mCol.[_NewEnum]
End Property
This is default Enumerator that is created when you use the Class Builder Utility.
However, the Attribute elements are not honored when the code is compiled. Using the OLE View program, it shows that the Attributes are ignored.
The XP computer produces the following snippet:
[id(0xfffffffc), propget, hidden]
HRESULT NewEnum([out, retval] IUnknown** );
But the Windows 7 computer generates:
[id(0x68030000), propget]
HRESULT NewEnum([out, retval] IUnknown** );
I have attempted to set the Visual Basic application to run in XP (Service Pack 3) Compatibility mode and this has not helped.
Because of the size and complexity of our application we cannot port it from VB6 to .NET at this time, and we need to get it to compile. Does anyone know how to properly configure VB6 so that it will properly compile a collection?
I have created a very small win32 console application (AddPrintMonitor.exe) that does nothing more than makes a call to AddMonitor. Here is a small snippet:
MONITOR_INFO_2 m_MonitorInfo2;
m_MonitorInfo2.pName = lpMonitorName;
TCHAR env[12] = TEXT("Windows x64");
m_MonitorInfo2.pEnvironment = env;
m_MonitorInfo2.pDLLName = lpDllName;
if ( !AddMonitor(NULL, 2, (LPBYTE) &m_MonitorInfo2) )
{
DWORD error = GetLastError();
std::cout << "Last error = " << error << "\n";
return PRINTER_ERR_API;
}
When I run this as a member of the Administrators group is fails. GetLastError() returns 5. When I run it as "The Administrator" it succeeds with no issues. I am running on Windows 7 x64. I am trying to install the redmonnt.dll and I do have the 64 bit version of that dll. This task is part of a larger install for a PostScript driver. I've only isolated out the AddMonitor portion to eliminate other external issues.
Error 5 is an access violation or security issue. My first question is why can't a member of the administrators group carry out this function call? What's the difference between the actual administrator and a member of the administrators group given this context?
Other details to note. I am using InstallShield 12 (old) for my Printer installation (addmonitor is only part). I am adding a monitor, port, driver and printer all through win32 function calls. Prior to running my AddPrinterMonitor.exe I made sure that the redmonnt.dll does exist in the system32 directory. Actually, whether the dll is present there or not makes no difference for the error I see. I did see posts about setting SeLoadDriverPrivilege. This is a dead end for me as I checked and the administrators group can Load/Unload device drivers.
Also, I am using win32 calls to get this done. This worked (I mean all of it worked) without any problems on Win2K, WinXP and Windows Server 2003. With the newer OS's like Windows 7 there are several difficulties. Is there a better way that I've overlooked? I been very frustrated trying to get this to work, so I've begun to question the approach for Vista and higher.
More initialization code:
#define MONITOR_NAME "My Redirected Port"
#define MONITOR_FILE "redmonnt.dll"
MONITOR_NAME is passed to lpMonitorName and MONITOR_FILE is passed to lpDllName
thanks
I've noticed that RTLMoveMemory seems to work just fine. But when I try to use RTLCopyMemory I get: "Can't find DLL entry point RtlCopyMemory in kernel32". Here is my declare:
Private Declare Sub CopyMem Lib "kernel32" Alias "RtlCopyMemory" ( _
ByVal dest As Long, _
ByVal source As Long, _
ByVal bytLen As Long)
RtlCopyMemory is provided inline. It is defined in winnt.h as memcpy. This means that it's not included in a Win32 DLL, it's part of the C runtime library. You could try importing memcpy from c:\windows\system32\msvcrt.dll.
Why not just use RtlMoveMemory? It works just like RtlCopyMemory except that it handles overlapped memory in a different fashion.
Bruce McKinney pioneered the use of RtlMoveMemory over 10 years ago and it's been standard for VB6 memory copying ever since.
I know this is an old question, but I had the same problem, so I thought I could answer.
RtlCopyMemory in kernel32.dll should be an Export Forward into ntdll,
but somehow guys at MS missed that one on x64 version of Vista (dunno how it's on x86) (see below).
You can try importing it directly from ntdll, if it's only for your needs.
EDIT: the method I mean is not exported, but with Symbols it's visible in ntdll as RtlCopyMemoryNonTemporal
EDIT2: Just to be sure I've checked some things, here's summary:
both RtlCopyMemory and RtlCopyMemoryNonTemporal are exported from ntdll.dll in x64 Vista's (plain, SP1, SP2)
there is export forward for RtlCopyMemory in kernel32.dll in x64 Vista's
there is unexported RtlCopyMemoryNonTemporal in ntdll.dll in x86 Vista's
So it all should be if you're writing x64 application.
P.S. I was wrong about x64 vs x86, because I was compiling x86 app, and running it on x64 (WOW mode),
so it used 32-bit version of kernel32, ntdll and not the x64 one.
I need to open a html help file from within a legacy windows application written in old version of C++ Builder. HtmlHelp is loaded via HtmlHelp.ocx, which I am loading via LoadLibrary.
This has worked fine for years, but it does not work anymore in Windows 7 x64. It might also fail under Windows7 x86, but I don't have any computer with this OS, so I can't try it out at the moment.
I am loading hhctrl.ocx dynamically as follows:
#define HHPathRegKey "CLSID\\{adb880a6-d8ff-11cf-9377-00aa003b7a11}\\InprocServer32"
bool THTMLHelper::LoadHtmlHelp()
{
HKEY HHKey;
DWORD PathSize = 255;
char Path[255];
bool R = false;
if (::RegOpenKeyExA(HKEY_CLASSES_ROOT, HHPathRegKey, 0, KEY_QUERY_VALUE, (void **)&HHKey) == ERROR_SUCCESS)
{
if (::RegQueryValueExA(HHKey, "", NULL, NULL, (LPBYTE)Path, &PathSize) == ERROR_SUCCESS)
{
//*****************************************
//LOADING FAILS HERE
//PATH IS %SystemRoot%\System32\hhctrl.ocx
//*****************************************
HHLibrary = ::LoadLibrary(Path);
if (HHLibrary != 0)
{
__HtmlHelp = (HTML_HELP_PROC) ::GetProcAddress(HHLibrary, "HtmlHelpA");
R = (__HtmlHelp != NULL);
if (!R)
{
::FreeLibrary(HHLibrary);
HHLibrary = 0;
}
}
}
::RegCloseKey(HHKey);
}
return R;
}
I checked if %SystemRoot%\System32\hhctrl.ocx exists on the Windows 7 system and it does.
Why does loading it via LoadLibrary fail? How can I work around this problem?
EDIT: GetLastError says (in German, so I am just translating): "Could not find file." But I debugged the function and the path is "%SystemRoot%\System32\hhctrl.ocx" and the file does exist.
Also, since two answers point in the direction of 64-bit vs 32-bit problems: My application is a 32 bit executable compiled in C++ Builder 5, so it should be a 32 bit process if I'm not mistaken. Or am I wrong to assume that?
Use ExpandEnvironmentStrings function to expand %SystemRoot%\System32\hhctrl.ocx to real path on user's intallation. 64bit OS will redirect expanded path to 32bit dll correctly.
You can't load 32bit dlls in a 64bit process, and visa versa. ActiveX controls are, of course, Dlls.
You can sometimes work around this by getting the 32bit ActiveX to load as an out-of-process server - its then hosted in a seperate 32bit (or 64bit) process as appropriate. This requires that the ActiveX onlyuses interfaces the system already knows how to marshal, and/or the project built 64bit AND 32bit versions of the proxy stub dll.
Depends is a tool that is very useful when you need to figure out why Dlls wont load. Of course, as a 32 bit application on a 64bit OS you need to know that 32 bit applications do NOT get access to %SYSTEMROOT%\System32 and, also do NOT read and write from HKCR directly. System32 actually contains the 64bit OS binaries, and HKCR contains the registry entries for 64bit apps.
A kernel process called 'reflection' redirects 32bit apps completely transparently to from System32 to %SYSTEMROOT%\SysWow64.
Likewise, registry access to HKEY_CLASSES_ROOT is redirected to `HKEY_CLASSES_ROOT\Wow6432Node'.
You need to know this of course, because explorer and regedit are 64bit processes and will happily show you the 64bit contents of System32 and HKCR. You need to explicitly navigate to the 32bit nodes to double check the view your 32bit process is going to get.
I have the exact same problem right now running W7 (x64).
I got it to work when I changed the "%SystemRoot%\System32\hhctrl.ocx" to "c:\windows\System32\hhctrl.ocx", but I guess I need to figure out why %SystemRoot% resolves wrong.
btw: I'm building a 32bit app on BCB2007.