Understanding .NET application options for 32 and 64-bit systems

Introduction

Around mid-nineties, the migration from 16-bit systems to 32-bit systems was in full swing. The benefits were very clear, but unfortunately, the move caused hair pulling for many developers as they needed to learn new memory models, new API functions and changed pointer arithmetic.

In the present day, we are facing a similar situation when moving from 32-bit to 64-bit systems. Luckily, this change is much more modest, and for many applications, not much needs to be done to keep the 32-bit application happily running on a 64-bit system. Because of the virtual machine technology and the way CLR (Common Language Runtime) is written, your .NET application might even automatically convert itself to a 64-bit application.

However, to break through the 2 GB memory limit or use native code or components outside the managed code barrier, you need to pay attention to the way you write your code.

In this article, you are going to learn the basics of moving your ,.NET & C# applications to 64-bit systems. Along the way, you are also going to learn a bit about memory management, code compatibility, and discover migration tips.

Note that this article uses the terms "32-bit system" and "64-bit system" in a specific manner. Here, these terms are used to indicate the operating system bitness, and not the bits supported by the computer hardware. Technically speaking, it is perfectly possible to install a 32-bit operating system on a PC capable of running 64-bit operating systems. In this article, bits refer to the installed architecture of the operating system.

The basics of application compatibility

Because the number of available Windows applications is enormous, a new Windows version cannot simply stop running older applications. The same applies when you migrate upwards in the number of bits in the system. Thus, a regular 32-bit application can in many cases continue to run on a 64-bit system without changes to the application code.

When installing, for instance, a 64-bit Windows 7 (Figure 1), the DLLs that implement the Windows programming interfaces are natively 64-bit. This might lead you to think that 32-bit applications would not be able to run, but in fact they can. This is because the operating system supports a feature called Windows-on-Windows (WoW64), which simulates a 32-bit Windows installation inside a 64-bit OS. Because of this, all the 32-bit applications think that they are running just as before, and in many cases work right out of the box.


Figure 1. System information about a 64-bit Windows 7 installation after pressing Win+Pause/Break.

All this applies well to native Windows applications, but .NET applications are somewhat different by default. Because of an additional layer of abstraction between the application and the operating system, the .NET runtime can decide how the intermediate language (IL) instructions are just-in-time (JIT) compiled to native code. Depending on the Windows operating system bitness, the CLR compiles the code to be either 32-bit or 64-bit native code. Thus, a .NET application compiled with the default settings will automatically reflect the number of bits in the underlying OS.

Although this is a very convenient behavior for the developer, it is not always the optimal one. Instead, you might wish to force your application to be a 32-bit or 64- bit one, depending on the circumstances. For instance, you might have dependencies in your application for native code components that are of certain number of bits. To be able to use these from your application, you usually must force an equal number of bits in your .NET application.

To select the number of bits you want in your application, it is best to go Microsoft Visual Studio's project options. For any application type (Figure 2 shows the properties of a Windows Presentation Foundation (WPF) application), Visual Studio shows in the project properties window (opened through the Project menu) a tab called Build. This tab in turn contains an option called Platform target. By default, the option is set to "Any CPU", meaning that the application will be compiled to native code depending on the number of bits in the operating system.


Figure 2. Visual Studio project options show the Platform target setting under the Build tab.

The other settable options are "x86" and "x64" for 32- and 64-bit builds, respectively. There's also the option to compile to the Itanium platform, but this platform is rare, and thus not the focus here. Remember that if you select for instance x64 as the platform target and try to run the application on a 32-bit system, the application won't run (Figure 3).


Figure 3. A 32-bit Windows 7 cannot run 64-bit applications.

Briefly about data types in .NET applications

If you as a .NET developer have experience in native code development as well as .NET application development, you might be interested in knowing what happens to your data types in .NET applications. In native code for example, an "integer" might be four bytes on a 32-bit system, but eight bytes on a 64-bit system, depending on your programming language.

In .NET applications, things are simpler. The basic numeric data types are always a certain number of bits: for instance, an int is always 32-bits and a short is always 16-bits. This makes programming easier when moving from 32-bit systems to 64-bit. This is different from many native programming languages where the basic data types like int often change their size when moving from 32-bit to 64-bit.

However, if you are working with native code technologies, such as Platform Invoke (P/Invoke), COM, or native code DLLs, you will need to understand the details about variable sizes. To help you in this process, .NET defines a special type called IntPtr.

This type, defined as a structure, is platform-specific. This means that its size changes depending on the platform target: it is four bytes on 32-bit builds and eight on 64- bit ones. You can use this type to conveniently store a pointer or a Windows handle, and have the same code work in both 32 and 64-bit systems. If you would try to use a .NET int value instead, you would see your code working on an x86 system, but failing on an x64 system.

About memory allocation limits in .NET applications

One of the primary reasons of moving to 64-bit architecture is the ability to use more memory. By default, 32-bit Windows applications are limited to 2 GB of memory (virtual address space). For many applications, this is enough, but high-end applications or those manipulating large amounts of data (be it images, video, statistical input or SQL databases) can run faster, if they can process larger amounts of memory without dividing the data into smaller chunks.

For .NET developers, understanding low-level memory management hasn't been necessary, as the CLR handles all the rudimentary work. Instead, it is enough to simply instantiate objects, and let the runtime handle the rest. The same applies in 64-bit systems, but even if you are running your .NET application on a 64-bit system, you cannot simply start allocating huge, multi-gigabyte arrays right away.

Instead, there's a limit on the size of any single object in .NET. For instance, you cannot allocate a byte array larger than approximately two gigabytes minus 64K. This limitation applies to both 32-bit and 64-bit applications. It would be great if you could allocate multi-gigabyte arrays in 64-bit applications, but unfortunately, this is not the case.

However, when your application is a 64-bit one (or runs on a 64-bit system when built with the Any CPU platform target), you can happily allocate multiple two gigabyte arrays without any problem, provided of course that there is enough system memory available.

Bear also in mind that the size limitation only applies to single objects. If you have an object that merely holds references (pointers) to other objects, like a list or a collection, you don't need to limit yourself to the roughly two gigabyte limit.

Calling native code from .NET applications

One of the great things about .NET application compatibility is that if you are using mainly .NET class library features and the Any CPU platform target, then you often don't need to worry about the operating system being a 32-bit or a 64-bit one. However, if you need to call COM objects, native DLL functions or the like, then you need to pay attention to the number of bits your application is built with.

For instance, assume you had written a 32-bit DLL that contains some legacy code that you don't want to or don't have the time to port to managed .NET code. If you would run your .NET application (built with the default settings as an Any CPU application) on a 64-bit system, your application would run as a 64-bit application, but the DLL would still be 32-bit. This will cause trouble, as a BadImageFormat exception will be raised with the message "An attempt was made to load a program with an incorrect format" (Figure 4).


Figure 4. A 64-bit process cannot load a 32-bit DLL directly.

In these situations, it is best to specifically select the correct platform target in Microsoft Visual Studio's project options. If your native code DLL would only be for instance 32-bit, then select x86 as the platform target. This will solve the compatibility issue, but on the other hand, you will lose the benefits of the additional memory a 64-bit system could provide.

If you can compile the DLL into a 64-bit version, then select x64 as the target for your .NET application. This can lead you to distribute two editions of your application. Alternatively, you could also dynamically load the correct DLL version based on the number of bits in the system. To detect the number of bits, you could use the methods shown in the next section.

Understanding .NET application options for 32 and 64-bit systems

Detecting the number of bits in the system

Especially when working with native/unmanaged code, it is beneficial to know how many bits the operating system runs natively with. Although there isn't a Windows API function or a .NET class library method to answer the question in a clean and simple manner, there are many ways to indirectly detect the number of bits in the system. Let's start with some of the Windows API functions available for the task.

If your application is a 32-bit one (built with the x86 platform target), then you can use the IsWow64Process API function to detect if your application is running as a 32- bit process under a 64-bit system under the Windows-on- Windows (WoW64) feature. The presence of WoW64 indicates that the operating system is a 64-bit one. If the process is running under WoW64, then the return value of the function is true; it is false otherwise. Here is an example implementation:

  [DllImport("kernel32.dll")]
  private static extern bool IsWow64Process(
       IntPtr hProcess,
       out bool wow64Process);
       
  internal static int DetectOSBitsMethod1()
  {
    bool isWowProcess;
    int bits;
    IntPtr processHandle =
      Process.GetCurrentProcess().Handle;
    if (IsWow64Process(processHandle, out isWowProcess))
    {
        bits = isWowProcess ? 64 : 32;
    }
    else bits = 32;
    return bits;
  }

Although the IsWow64Process API function is convenient, it won't help much if your .NET application is built with the Any CPU or x64 platform targets. In these cases, you could use the GetSystemInfo API function. This function returns a structure of details about memory and the processor(s) in the system. A field named lpMaximumApplicationAddress returns the highest pointer value for a process' virtual address space.

For 32-bit applications, this value is by default roughly 2 GB (2^31), but even with the large address space extensions enabled (through the /3GB boot time switch, to enable a feature called 4 GT RAM Tuning), it is never above 4 GB (2^32). For 64-bit applications, the value depends on the OS version and edition, but is in the range of 8 TB (2^43).

Thus, you could check to see if the value is larger than 4 GB, and if so, you can be pretty confident that the system is a 64-bit one if the platform target is Any CPU:

  [DllImport("kernel32.dll")]
  private static extern void GetSystemInfo(
      ref SYSTEM_INFO lpSystemInfo);
  
  [StructLayout(LayoutKind.Sequential)]
  private struct SYSTEM_INFO
  {
      internal _PROCESSOR_INFO_UNION uProcessorInfo;
      public uint dwPageSize;
      public IntPtr lpMinimumApplicationAddress;
      public IntPtr lpMaximumApplicationAddress;
      public IntPtr dwActiveProcessorMask;
      public uint dwNumberOfProcessors;
      public uint dwProcessorType;
      public uint dwAllocationGranularity;
      public ushort dwProcessorLevel;
      public ushort dwProcessorRevision;
  }
  
  [StructLayout(LayoutKind.Explicit)]
  private struct _PROCESSOR_INFO_UNION
  {
      [FieldOffset(0)]
      internal uint dwOemId;
      [FieldOffset(0)]
      internal ushort wProcessorArchitecture;
      [FieldOffset(2)]
      internal ushort wReserved;
  }
  
  internal static int DetectOSBitsMethod2()
  {
      SYSTEM_INFO sysInfo = new SYSTEM_INFO();
      GetSystemInfo(ref sysInfo);
      const long FourGB = (long)4 * 1024 * 1024 * 1024;
      long addrMax = sysInfo.
        lpMaximumApplicationAddress.ToInt64();
      int bits = (addrMax <= FourGB) ? 32 : 64;
      return bits;
  }

With managed .NET code alone, you can use similar indirect methods to detect the number of bits in the system. Probably the easiest way to do this is to check the size of the IntPtr data type. This type reflects the native pointer size of the system, and is four on a 32-bit system and eight on a 64-bit system. Again for the Any CPU platform target:

  internal static int DetectOSBitsMethod3()
  {
      const int BitsInByte = 8;
      int bits = BitsInByte * IntPtr.Size;
      return bits;
  }

Another .NET based option would be to check environment variables. Specifically, the variable named PROCESSOR_ARCHITECTURE usually indicates the correct system architecture. On a 32-bit system, the value is "x86", and on a 64-bit system, it is "AMD64":

  internal static int DetectOSBitsMethod4()
  {
      string processorArchitecture =
          System.Environment.GetEnvironmentVariable(
          "PROCESSOR_ARCHITECTURE",
          EnvironmentVariableTarget.Machine);
      int bits = (processorArchitecture == "x86") ? 32 : 64;
      return bits;
  }

Finally, there are certain registry keys that are usually only present on 64-bit Windows installations. Then, you could check for the existence of these keys from code. For instance, the registry key "HKEY_LOCAL_MACHINE\Software\Wow6432Node" exists only on 64- bit systems (on 32-bit systems, this key simply isn't there). You could use the following code to check for the key (this works regardless of the platform target setting):

  internal static int DetectOSBitsMethod5()
  {
    int bits = 32;
    try
    {
      RegistryKey key = Registry.LocalMachine.
          OpenSubKey(@"Software\Wow6432Node");
      if (key != null)
      {
        bits = 64;
        key.Close();
      }
    }
    catch
    {
        // do nothing
    }
    return bits;
  }

As you can see, all these methods are indirect and approximate at best, and prone to failures. However, they work well in most situations, and thus could be used with reasonable confidence. It is also worth remembering that the need to detect system bits is transient in nature: in the future, most systems will be 64-bit ones, and thus you could only deliver a 64-bit version of your applications by setting the platform target property to x64. Then, Windows itself will make sure your application won't run on any 32- bit system.

Conclusion

Moving .NET applications to 64-bit systems is generally easy. If you are using only managed class library features, then in the best case, you would not need to do a thing to make sure your application runs properly on any 64-bit system.

However, as your applications become more complex, it is likely that you need to pay attention to whether your application uses the Any CPU platform target, or the specific x86 or x64 targets. This is especially true when working with unmanaged (native) code components, such as DLLs. In these situations, you need to select the correct platform target for your application.

As more and more systems start to be 64-bit, the easier it is to overcome the infamous 2 GB memory limit. Although .NET currently limits the size of single objects to around 2 GB, you can easily use multiple objects. This way, you can break free from the limits that 32-bit systems have. In this article, you learned the basics of moving your code from 32- bit to 64-bit, and what this means for .NET developers. You also saw how interoperability is affected, and how you could at least approximately detect whether the system is a 32-bit or a 64-bit one. With this information, you should be well equipped to migrate to 64-bit .NET code.

Happy hacking!
Jani Jarvinen



About the Author

Jani Jarvinen

Jani Jarvinen is a software development trainer and consultant in Finland. He is a Microsoft C# MVP, a frequent author and has published three books about software development. He is the group leader of a Finnish software development expert group at ITpro.fi and a board member of the Finnish Visual Studio Team System User Group. His blog can be found at http://www.saunalahti.fi/janij/. You can send him mail by clicking on his name at the top of the article.

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

  • A modern mobile IT strategy is no longer an option, it is an absolute business necessity. Today's most productive employees are not tied to a desk, an office, or a location. They are mobile. And your company's IT strategy has to be ready to support them with easy, reliable, 24/7 access to the business information they need, from anywhere in the world, across a broad range of communication devices. Here's how some of the nation's most progressive corporations are meeting the many needs of their mobile workers …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds