Application Modernization: What Is It and How to Get Started
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.
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
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
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
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.