![]() |
< Day Day Up > |
![]() |
1.2. Common Language RuntimeThe Common Language Runtime manages the entire life cycle of an application: it locates code, compiles it, loads associated classes, manages its execution, and ensures automatic memory management. Moreover, it supports cross-language integration to permit code generated by different languages to interact seamlessly. This section peers into the inner workings of the Common Language Runtime to see how it accomplishes this. It is not an in-depth discussion, but is intended to make you comfortable with the terminology, appreciate the language-neutral architecture, and understand what's actually happening when you create and execute a program. Compiling .NET CodeCompilers that are compliant with the CLR generate code that is targeted for the runtime, as opposed to a specific CPU. This code, known variously as Common Intermediate Language (CIL), Intermediate Language (IL), or Microsoft Intermediate Language (MSIL), is an assembler-type language that is packaged in an EXE or DLL file. Note that these are not standard executable files and require that the runtime's Just-in-Time (JIT) compiler convert the IL in them to a machine-specific code when an application actually runs. Because the Common Language Runtime is responsible for managing this IL, the code is known as managed code. This intermediate code is one of the keys to meeting the .NET Framework's formal objective of language compatibility. As Figure 1-3 illustrates, the Common Language Runtime neither knows梟or needs to know梬hich language an application is created in. Its interaction is with the language-independent IL. Because applications communicate through their IL, output from one compiler can be integrated with code produced by a different compiler. Figure 1-3. Common Language Runtime functionsAnother .NET goal, platform portability, is addressed by localizing the creation of machine code in the JIT compiler. This means that IL produced on one platform can be run on any other platform that has its own framework and a JIT compiler that emits its own machine code. In addition to producing IL, compilers that target the CLR must emit metadata into every code module. The metadata is a set of tables that allows each code module to be self-descriptive. The tables contain information about the assembly containing the code, as well as a full description of the code itself. This information includes what types are available, the name of each type, type members, the scope or visibility of the type, and any other type features. Metadata has many uses:
IL and metadata are crucial to providing language interoperability, but its real-world success hinges on all .NET compilers supporting a common set of data types and language specifications. For example, two languages cannot be compatible at the IL level if one language supports a 32-bit signed integer and the other does not. They may differ syntactically (for example, C# int versus a Visual Basic Integer), but there must be agreement of what base types each will support. As discussed earlier, the CLI defines a formal specification, called the Common Type System (CTS), which is an integral part of the Common Language Runtime. It describes how types are defined and how they must behave in order to be supported by the Common Language Runtime. Common Type SystemThe CTS provides a base set of data types for each language that runs on the .NET platform. In addition, it specifies how to declare and create custom types, and how to manage the lifetime of instances of these types. Figure 1-4 shows how .NET organizes the Common Type System. Figure 1-4. Base types defined by Common Type SystemTwo things stand out in this figure. The most obvious is that types are categorized as reference or value types. This taxonomy is based on how the types are stored and accessed in memory: reference types are accessed in a special memory area (called a heap) via pointers, whereas value types are referenced directly in a program stack. The other thing to note is that all types, both custom and .NET defined, must inherit from the predefined System.Object type. This ensures that all types support a basic set of inherited methods and properties. Core Note
A compiler that is compliant with the CTS specifications is guaranteed that its types can be hosted by the Common Language Runtime. This alone does not guarantee that the language can communicate with other languages. There is a more restrictive set of specifications, appropriately called the Common Language Specification (CLS), that provides the ultimate rules for language interoperability. These specifications define the minimal features that a compiler must include in order to target the CLR. Table 1-1 contains some of the CLS rules to give you a flavor of the types of features that must be considered when creating CLS-compliant types (a complete list is included with the .NET SDK documentation).
These rules are both straightforward and specific. Let's look at a segment of C# code to see how they are applied: public class Conversion { public double Metric( double inches) { return (2.54 * inches); } public double metric( double miles) { return (miles / 0.62); } } Even if you are unfamiliar with C# code, you should still be able to detect where the code fails to comply with the CLS rules. The second rule in the table dictates that different names must differ by more than case. Obviously, MeTRic fails to meet this rule. This code runs fine in C#, but a program written in Visual Basic.NET梬hich ignores case sensitivity梬ould be unable to distinguish between the upper and lowercase references. AssembliesAll of the managed code that runs in .NET must be contained in an assembly. Logically, the assembly is referenced as one EXE or DLL file. Physically, it may consist of a collection of one or more files that contain code or resources such as images or XML data. An assembly is created when a .NET compatible compiler converts a file containing source code into a DLL or EXE file. As shown in Figure 1-5, an assembly contains a manifest, metadata, and the compiler-generated Intermediate Language (IL). Let's take a closer look at these:
Figure 1-5. Single file assemblyThe assembly is more than just a logical way to package executable code. It forms the very heart of the .NET model for code deployment, version control, and security:
As mentioned, an assembly may contain multiple files. These files are not restricted to code modules, but may be resource files such as graphic images and text files. A common use of these files is to permit resources that enable an application to provide a screen interface tailored to the country or language of the user. There is no limit to the number of files in the assembly. Figure 1-6 illustrates the layout of a multi-file assembly. Figure 1-6. Multi-file assemblyIn the multi-file assembly diagram, notice that the assembly's manifest contains the information that identifies all files in the assembly. Although most assemblies consist of a single file, there are several cases where multi-file assemblies are advantageous:
Multi-file assemblies can be created by executing the C# compiler from the command line or using the Assembly Linker utility, Al.exe. An example using the C# compiler is provided in the last section of this chapter. Notably, Visual Studio.NET 2005 does not support the creation of multi-file assemblies. Private and Shared AssembliesAssemblies may be deployed in two ways: privately or globally. Assemblies that are located in an application's base directory or a subdirectory are called privately deployed assemblies. The installation and updating of a private assembly could not be simpler. It only requires copying the assembly into the directory, called the AppBase, where the application is located. No registry settings are needed. In addition, an application configuration file can be added to override settings in an application's manifest and permit an assembly's files to be moved within the AppBase. A shared assembly is one installed in a global location, called the Global Assembly Cache (GAC), where it is accessible by multiple applications. The most significant feature of the GAC is that it permits multiple versions of an assembly to execute side-by-side. To support this, .NET overcomes the name conflict problem that plagues DLLs by using four attributes to identify an assembly: the file name, a culture identity, a version number, and a public key token. Public assemblies are usually located in the assembly directory located beneath the system directory of the operating system (WINNT\ on a Microsoft Windows 2000 operating system). As shown in Figure 1-7, the assemblies are listed in a special format that displays their four attributes (.NET Framework includes a DLL file that extends Windows Explorer to enable it to display the GAC contents). Let's take a quick look at these four attributes:
Figure 1-7. Partial listing of Global Assembly DirectoryCore Note
Precompiling an AssemblyAfter an assembly is loaded, the IL must be compiled to the machine's native code. If you are used to working with executables already in a machine code format, this should raise questions about performance and whether it's possible to create equivalent "executables" in .NET. The answer to the second part of the statement is yes; .NET does provide a way to precompile an assembly. The .NET Framework includes a Native Image Generator (Ngen) tool that is used to compile an assembly into a "native image" that is stored in a native image cache梐 reserved area of the GAC. Any time the CLR loads an assembly, it checks the cache to see if it has an associated native image available; if it does, it loads the precompiled code. On the surface, this seems a good idea to improve performance. However, in reality, there are several drawbacks. Ngen creates an image for a hypothetical machine architecture, so that it will run, for example, on any machine with an x86 processor. In contrast, when the JIT in .NET runs, it is aware of the specific machine it is compiling for and can accordingly make optimizations. The result is that its output often outperforms that of the precompiled assembly. Another drawback to using a native image is that changes to a system's hardware configuration or operating system梥uch as a service pack update梠ften invalidate the precompiled assembly. Core Recommendation
Code VerificationAs part of the JIT compile process, the Common Language Runtime performs two types of verification: IL verification and metadata validation. The purpose is to ensure that the code is verifiably type-safe. In practical terms, this means that parameters in a calling and called method are checked to ensure they are the same type, or that a method returns only the type specified in its return type declaration. In short, the CLR searches through the IL and metadata to make sure that any value assigned to a variable is of a compatible type; if not, an exception occurs. Core Note
A benefit of verified code is that the CLR can be certain that the code cannot affect another application by accessing memory outside of its allowable range. Consequently, the CLR is free to safely run multiple applications in a single process or address space, improving performance and reducing the use of OS resources. |
![]() |
< Day Day Up > |
![]() |