[ Team LiB ] |
1.2 The Common Language RuntimeThe Common Language Runtime (CLR) is the environment in which all programs run in .NET, so it affects everything we do as developers. It is therefore important to understand what it does and what that means to our programs, so we will now look at the most important features of the CLR. All high-level programming languages have a runtime. This is because an OS process provides a fairly low-level set of features, typically just memory, pointers, threads, machine code, and system calls. The job of any language's runtime is to bridge the gap between these OS facilities and the constructs defined by the programming language. In C++, the runtime provides such features as exception handling, runtime type information, and the standard C++ library. The Visual Basic runtime includes intrinsic handling for COM and automatic memory management. Traditionally, each language has provided its own runtime, such as MSVCRT.DLL for C++, MSVBVM60.DLL for Visual Basic, and the Java Virtual Machine (MSJAVA.DLL, if you're using Microsoft's VM) for Java. In .NET, there is just one runtime, which is used by all languages, the CLR. The fact that all languages use this one runtime is important for a number of reasons. It means components can easily be written in and used from any language because all languages represent types and objects in the same way. (Anyone familiar with COM in C++ will be amazed at how simple it is to write and use .NET components.) Moreover, it means that all languages can use the same API to access the platform's services. 1.2.1 Managed CodeC++ developers are used to writing low-level code. All the abstractions with which Win32 assembly language developers work (virtual memory, threads of execution, numbers, and pointers) are also exposed directly in C++. (Yes, there are still a few diehards who insist on writing Win32 applications in assembly language.) The C++ compiler supplies a useful veneer on top of these, providing facilities such as object-oriented programming and optional type safety, but the fact remains that all the platform's lowest level details are visible. For many developers, this is the appeal of C++—it hides nothing, imbuing the developer with a feeling of ultimate power. But this power has its price. First, there is the amount of effort required to wield this power. Java and Visual Basic developers often marvel at how much time C++ developers seem to spend writing code to deal with things that simply aren't an issue in higher-level languages. Second, the power of C++ is often its Achilles' heel—C++ offers opportunities to crash and burn that are simply not available elsewhere. Very often, these costs are not outweighed by the benefits because, in practice, the full power of C++ is rarely required. Software development has been using more and more abstraction throughout its history. A few years ago, it would have been considered essential for performance-critical parts of an application to be written in assembly language, but this practice has all but disappeared, because computers are now fast enough that it is rarely worth the extra development costs. Likewise, less than a decade ago, PC applications ran without the crash protection offered by modern operating systems and had absolute power over the whole machine. These days, we all benefit from the improved robustness achieved by abandoning that level of control and running most applications in secure sandboxed processes. If an application crashes, it no longer takes all the other applications on the machine with it—only the operating system and its device drivers need to live in the dangerous world of the kernel, where one false move can bring the entire machine to its knees. Most people welcome the resultant improvement in productivity and consider it to be worth the slight loss of control. .NET takes a step forward that is very similar to the transition from assembly language to high-level languages, or the move from DOS-style operating systems to more reliable and secure modern operating systems. Once again it involves a slight loss of control in exchange for higher productivity, so we will inevitably hear the same kind of lamentation as we did when these older technologies were marginalized. But most developers don't find the loss of control to be a big deal in practice. The name given to this advance is managed code or managed execution. Managed code is code that does not use the abstractions of assembly language—it deals with higher-level constructs. The most important difference is that the environment in which managed code runs has an intrinsic type system. With unmanaged code, Win32 simply gives us raw virtual memory and lets our programs use the processor to do whatever we like with that memory. Nothing stops code from storing a floating-point number in some memory location and then trying to read it as though it were a pointer—the code would be allowed to proceed, despite the fact that the binary value of the floating-point number will make no sense as a pointer and the code will almost certainly either crash or malfunction. But in .NET, this is not allowed to happen, because all information is strongly typed in the CLR. It knows whether a particular piece of memory represents, say, an integer or a floating-point number or an object reference, and actively prevents us from misinterpreting that data. The .NET managed runtime prevents such code from running in the first place—all code must pass type-safety verification before it is allowed to execute. The runtime also has an intrinsic understanding of concepts such as objects, strings, heaps, and components. This means that compiled programs look very different under .NET—as we will now see, the very nature of the binaries generated by compilers has changed. 1.2.2 Compilation in .NETTo examine the new features of the runtime and to get a feel for how they change the world our programs live in, here's a simple program that runs under .NET. We will of course start with the canonical first program in C#: class Hello { static void Main() { System.Console.WriteLine("Hello, World"); } }
If you compile this and examine the output of the compiler with a disassembler (such as the ILDASM tool that ships with the .NET Framework SDK) you will find that it is very different from the output of the old C++ compiler, as Figure 1-1 shows. The most immediately obvious difference is that the compiled code contains type information—the first thing ILDASM presents is a tree view of the types defined inside the component. We will look into the nature of this type information shortly, but first we will look at some compiled code. Figure 1-1. A simple program in ILDASMIf you expand the tree in ILDASM and double-click on any method, it will show the disassembled code of that method in a new window. Here is the compiled code for the Main method defined above: ldstr "Hello, world!" call void [mscorlib]System.Console::WriteLine(string) ret This shows the second most striking difference between the output from a traditional compiler and the code that a .NET compiler generates. This is not assembly language for an Intel processor—instead of strings, type names, and method signatures, the operands in disassembled Pentium code would just be so many hexadecimal digits. In fact, no processor is capable of running this code directly. Code in .NET binaries is stored in a so-called Intermediate Language (IL or CIL, as it is sometimes abbreviated), which the runtime will translate into the processor's native machine code to execute it. All languages compile into IL, so the equivalent Visual Basic program would look very similar in ILDASM. All managed code is compiled into IL. It is similar in nature to Java's bytecode in that it is a processor-independent machine language that supports type-safe object-oriented programming. As a quick glance at this example has already shown, it is very different from Pentium code. Looking at the first line, it is clear that strings are supported as an intrinsic data type. The second line contains evidence that type information permeates .NET code even at the lowest level—in unmanaged (i.e., pre-.NET) code, a call instruction would simply contain the address of the function it was calling; here, it contains the name of the method (WriteLine), and also the name of the class the method belongs to (System.Console), and the component in which that class is defined (mscorlib; more on components shortly). Furthermore, the signature of the method is present—this call instruction is clearly expecting to call a method that takes a single parameter of type string and has a void return type. Type information is embedded this deeply throughout all managed code. This is what enables the CLR to verify that code does not break any of the type safety rules—all managed code is required to be explicit about the types it is using at all times. Of course the problem with this code is that there is no CPU in the world that can execute it. So one of the services the runtime must provide is a way of bridging the gap between this strongly typed code and the world of raw, untyped memory in which the processor lives. It does this by compiling the IL into native code on demand. This is done one method at a time—code will only be compiled when it is needed (i.e., the first time a method is called). This compilation process is therefore known as just-in-time compilation (JIT). The type safety verification tests are applied before JIT happens, which means the JIT compiler doesn't need to generate code that wastes a lot of time enforcing the CLR type system's rules; it checks the code just once, up front. And the JIT compiler has a lot in common with the code generator used by the standard unmanaged C++ compiler, so the performance of code in the CLR turns out to be almost as good as that of code compiled in the traditional way. The main cost is that methods are much slower the very first time you run them, because they need to be compiled before they can run; also, the JIT-compiled code is discarded when the program exits, so this compilation cost is paid every time the program runs. Fortunately, this happens quickly (typically within a few milliseconds), so it's not slow enough to cause a perceptible slow down. And remember that this price is only paid the first time the method is called—for every subsequent call, the compiled code is used. So for long-running applications, the proportion of time spent in the JIT compiler is negligibly small. For some applications, the slower startup that can be caused by JIT compilation may be a problem. For such programs, .NET provides a facility that allows components to be precompiled at installation time, so no JIT compilation needs to occur at runtime. This facility is called NGEN (short for native code generation), and certain critical system libraries (including Windows Forms) use it. However, this causes programs to take up considerably more disk space, and under some circumstances it can increase memory usage, so you should not use this feature unless you have identified startup time as a problem and your tests show that NGEN actually improves matters. Note that NGEN is not a viable way of making reverse engineering harder—an NGENed binary still contains all the IL and type information from the original. It is not possible to remove this. In fact, there are performance benefits to JIT compilation. Only the code that needs to run is compiled, which can reduce a process's working set. Furthermore, applications always get the benefit of the latest compiler technology, whereas a traditional application is stuck with whatever the state of the art was when it was compiled. Furthermore, as 64-bit systems become more widespread, managed code will be ready for them, as the CLR will just generate native 64-bit code from the IL instead of 32-bit code. This should make the transition from 32-bit to 64-bit systems considerably less painful than the decade-long transition from 16-bit to 32-bit systems. This also makes it possible for components to work both on normal PCs and on mobile systems that support the Compact .NET Framework, even though these typically use an entirely different processor architecture. But the single most important aspect of IL is that it is permeated with type information, and the type system is arguably the most significant feature of the .NET runtime. 1.2.3 The Role of the Type SystemWith the classic C++ compilation model, the type system was for the most part something that only existed during compilation. The compiler typically did its best to remove as much evidence of the types used in the source code as possible. There would inevitably be some residue; for example if Runtime Type Information (RTTI) was enabled, objects would be annotated with type information, but it was somewhat minimal. For example, given a reference to an object, you couldn't find out at runtime what fields and methods it contained, or what their types and signatures were. The vast majority of the type information present in the source would be gone by runtime. As we have already seen, this is not the case with .NET. The ILDASM tool presents us with a tree view showing every single type defined in the component, and provides full information on the contents of these types. This even includes members marked as private. And as we have seen, compiled code also contains full information about the types it is trying to use.
This ubiquitous nature of type information is fundamental to many of the services .NET provides. Because absolutely everything is fully annotated with type information (and this information is accessible at runtime through the reflection API), it is possible for the runtime to automate many facilities in a way that was not previously possible. For example, the runtime can automatically serialize objects by examining the type information to find out what fields are present and what their types are. The remoting services examine method definitions at runtime to determine how to make them work over the network. Service descriptions for web services are generated by the system automatically by analyzing the classes that provide those services. To make use of this type system, we will of course need a programming language. A wide range of common languages is available for .NET, but we will now look at two .NET-specific languages, C# and Visual Basic .NET. |
[ Team LiB ] |