In a great number of cases, there is a simple one-to-one correspondence between a .NET assembly
and the binary file (*.dll or *.exe). Thus, if you are building a .NET *.dll, it is safe to consider that
the binary and the assembly are one and the same. Likewise, if you are building an executable desktop
application, the *.exe can simply be referred to as the assembly itself. As you’ll see in Chapter 11,
however, this is not completely accurate. Technically speaking, if an assembly is composed of a single
*.dll or *.exe module, you have a single-file assembly. Single-file assemblies contain all the necessary
CIL, metadata, and associated manifest in an autonomous, single, well-defined package.
Multifile assemblies, on the other hand, are composed of numerous .NET binaries, each of which
is termed a module. When building a multifile assembly, one of these modules (termed the primary
module) must contain the assembly manifest (and possibly CIL instructions and metadata for various
types). The other related modules contain a module level manifest, CIL, and type metadata. As you
might suspect, the primary module documents the set of required secondary modules within the
assembly manifest.
So, why would you choose to create a multifile assembly? When you partition an assembly into
discrete modules, you end up with a more flexible deployment option. For example, if a user is referencing a remote assembly that needs to be downloaded onto his or her machine, the runtime will only download the required modules. Therefore, you are free to construct your assembly in such a way that less frequently required types (such as a type named HardDriveReformatter) are kept in a separate stand-alone module.
In contrast, if all your types were placed in a single-file assembly, the end user may end up
downloading a large chunk of data that is not really needed (which is obviously a waste of time).
Thus, as you can see, an assembly is really a logical grouping of one or more related modules that
are intended to be initially deployed and versioned as a single unit.
The Role of the Common Intermediate Language
Now that you have a better feel for .NET assemblies, let’s examine the role of the common
intermediate language (CIL) in a bit more detail. CIL is a language that sits above any particular
platform-specific instruction set. Regardless of which .NET-aware language you choose, the
associated compiler emits CIL instructions. For example, the following C# code models a trivial
calculator. Don’t concern yourself with the exact syntax for now, but do notice the format of the
Add() method in the Calc class:
// Calc.cs
using System;
namespace CalculatorExample
{
// This class contains the app's entry point.
public class CalcApp
{
static void Main()
{
Calc c = new Calc();
int ans = c.Add(10, 84);
Console.WriteLine("10 + 84 is {0}.", ans);
// Wait for user to press the Enter key before shutting down.
Console.ReadLine();
}
}
// The C# calculator.
public class Calc
{
public int Add(int x, int y)
{ return x + y; }
}
}
Once the C# compiler (csc.exe) compiles this source code file, you end up with a single-file
*.exe assembly that contains a manifest, CIL instructions, and metadata describing each aspect of
the Calc and CalcApp classes. For example, if you were to open this assembly using ildasm.exe
(examined a little later in this chapter), you would find that the Add() method is represented using
CIL such as the following:
.method public hidebysig instance int32 Add(int32 x, int32 y) cil managed
{
// Code size 8 (0x8)
.maxstack 2
.locals init ([0] int32 CS$1$0000)
IL_0000: ldarg.1
IL_0001: ldarg.2
IL_0002: add
IL_0003: stloc.0
IL_0004: br.s IL_0006
IL_0006: ldloc.0
IL_0007: ret
} // end of method Calc::Add
Don’t worry if you are unable to make heads or tails of the resulting CIL for this method—
Chapter 15 will describe the basics of the CIL programming language. The point to concentrate on
is that the C# compiler emits CIL, not platform-specific instructions.
Now, recall that this is true of all .NET-aware compilers. To illustrate, assume you created this
same application using Visual Basic .NET (VB .NET), rather than C#:
' Calc.vb
Imports System
Namespace CalculatorExample
' A VB .NET 'Module' is a class that only contains
' static members.
Module CalcApp
Sub Main()
Dim ans As Integer
Dim c As New Calc
ans = c.Add(10, 84)
Console.WriteLine("10 + 84 is {0}.", ans)
Console.ReadLine()
End Sub
End Module
Class Calc
Public Function Add(ByVal x As Integer, ByVal y As Integer) As Integer
Return x + y
End Function
End Class
End Namespace
If you examine the CIL for the Add() method, you find similar instructions (slightly tweaked by
the VB .NET compiler):
.method public instance int32 Add(int32 x, int32 y) cil managed
{
// Code size 9 (0x9)
.maxstack 2
.locals init ([0] int32 Add)
IL_0000: nop
IL_0001: ldarg.1
IL_0002: ldarg.2
IL_0003: add.ovf
IL_0004: stloc.0
IL_0005: br.s IL_0007
IL_0007: ldloc.0
IL_0008: ret
} // end of method Calc::Add
Benefits of CIL
At this point, you might be wondering exactly what is gained by compiling source code into CIL
rather than directly to a specific instruction set. One benefit is language integration. As you have
already seen, each .NET-aware compiler produces nearly identical CIL instructions. Therefore, all languages are able to interact within a well-defined binary arena.
Furthermore, given that CIL is platform-agnostic, the .NET Framework itself is platform-agnostic, providing the same benefits Java developers have grown accustomed to (i.e., a single code base running on numerous operating systems). In fact, there is an international standard for the C# language, and a large subset of the .NET platform and implementations already exist for many non-Windows operating systems (more details at the conclusion of this chapter). In contrast to Java, however, .NET allows you to build applications using your language of choice.
Compiling CIL to Platform-Specific Instructions
Due to the fact that assemblies contain CIL instructions, rather than platform-specific instructions, CIL code must be compiled on the fly before use. The entity that compiles CIL code into meaningful CPU instructions is termed a just-in-time (JIT) compiler, which sometimes goes by the friendly name of Jitter. The .NET runtime environment leverages a JIT compiler for each CPU targeting the runtime, each optimized for the underlying platform.
For example, if you are building a .NET application that is to be deployed to a handheld
device (such as a Pocket PC), the corresponding Jitter is well equipped to run within a lowmemory environment. On the other hand, if you are deploying your assembly to a back-end
server (where memory is seldom an issue), the Jitter will be optimized to function in a highmemory environment. In this way, developers can write a single body of code that can be
efficiently JIT-compiled and executed on machines with different architectures.
Furthermore, as a given Jitter compiles CIL instructions into corresponding machine code, it
will cache the results in memory in a manner suited to the target operating system. In this way, if a call is made to a method named PrintDocument(), the CIL instructions are compiled into platformspecific instructions on the first invocation and retained in memory for later use. Therefore, the next time PrintDocument() is called, there is no need to recompile the CIL.
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment