How can a VB 6 app determine if it is running on 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:

  1. 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.

  2. 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.