Assemblies, Threads, & App Domains

Contents

  1. Problems with COM Binaries
  2. Overview of .NET Assemblies
  3. Single File Assembly - Test
  4. Cross-Language Inheritance
  5. Exploring the Manifest
  6. Exploring Types
  7. Understanding Private Assemblies
  8. Understanding Shared Assembly
  9. Understanding Shared (Strong) Names
  10. Using a Shared Assembly
  11. Understanding .NET Version Policies
  12. System.AppDomain and Threading

Problems with COM Binaries

COM Versioning

COM runtime offers no intrinsic support to enforce that the correct vesion of a COM binary is loaded for the calling client. All tasks related to versioning are left to the programmer.

In contrast, .NET allows multiple versions of the same binary to be installed on the same machine, If client A demands MyServer.dll version 1.0 while client B demands MyServer.dll version 2.0, the correct version from the same binary is loaded for the respective client immediately.

COM Deployment

The relation between the binary file and the correct registry entries is very loose. If the file was moved or renamed, the entire system breaks. Also, if a DLL is installed on a remote server machine and that machine is accessed by 100 clients, then 101 machines must be configured correctly.

In contrast, .NET makes deployment very easy given the fact that .NET binaries are not registered in the system registry at all. Deploying a .NET application is as simple as copying the files that compose the application to some location on the machine and running the application.

Overview of .NET assemblies

A .NET application is a collection of assemblies. An assembly is a versioned, self-describing binary (DLL or EXE) containing collection of types (classes, structures, etc) and optional resources (images, string tables, etc.). .NET DLLs only require one function export, DllMain().

From Basics section , recall that assemblies are .NET binaries that contain IL, metadata, and manifest data. The following points summarize assemblies

/* C++: #using is similar to #import but it does more. It does the work of the following lines for an equivalent unmanaged C++ application:
    #include <windows.h>
    #pragma comment(lib, "user32.lib")
    #pragma comment(lib, "kernel.lib")

In other words, the using line imports the description of types from an assembly so that the compiler can perform compile-time checking. In addition, it instructs the compiler to add tables to the final exe that identify the libraries the code will use (equivalent to the linker instructions passed through the #pragma comment directive) */

#using <mscorlib.dll>

/* this tells C++ that when you use the types in the System namespace, you do not want to full qualify the type name with its namespace */
using namespace System;

Assemblies - Single-file and Multi-file 

Recall that a single file assembly contains all the necessary IL, metadata, and associated manifest in a single well-define package, whereas multi-file assemblies are composed of numerous .NET binaries, each of which is termed a module:

A multi-file assembly is an assembly composed of multiple files. A multi-file assembly encourages efficient code download. Assume a multi file assembly is composed of 2 DLLs and  a remote client references only one of those DLLs.. The .NET runtime will only download the required DLL (imagine if each DLL was 2 MB in size)

Recall that A multi-file assembly contains more than one DLL that are logically related by information contained in the corresponding manifest. From the above figure, a multi file assemble contain a single manifest that may be placed in a stand-alone file but is more typically bundled into one of the related modules. The following figure also shows a sample schematic of a multifile assembly:

An assembly called one is made up of four separate files - one.dll, two.netmodule, three.netmodule, and four.jpg. The main file is one.dll which contains the manifest for the entire assembly. The short name of the assembly is the name of this file without the extension, so this is assembly one. Note that the assembly manifest indicates that the assembly one is made up of three other files beside one.dll. In addition, the manifest  has references to two other assemblies, five.dll and mscorlib.dll. Finally, the assembly one uses localize resources in satellite assemblies. These are resource only assemblies, one of which which contains the right locale, is loaded at runtime by an object called ResourceManager.

Note that if assembly one is downloaded over the network, the physical file one.dll will be downloaded, but not necessarily the other modules. Only if your code refers to one of the other modules in assembly one or uses resources, will these files be downloaded.

.NET provides several way to determine where code is located, and it even allows you to change the location through configuration files. A module cannot be loaded by itself, it needs to be loaded only as part of an assembly. To create a module in C# use /targert:module switch or if in C++ pass the /NOASSEMBLY switch to the linker. To create an assembly, you need to link all the modules together. In C# you can use /target:library or /target:exe or /target:winexe.  In C++, it is similar except you have to inform the linker with the /DLL switch to create a library assembly or you can use /SUBSYSTEM with CONSOLE or WINDOWS.

Multimodule assemblies are a great way of streamlining how your code will be distributed. Unfortunately, Visual Studi.NET does not support multimodule assemblies. You will have to write your own make files.

Assemblies - Logical & Physical Views

Conceptually, an assembly can be have two views. Physically, an assembly is a collection of files. Logically, as assembly is a collection of types.

 

 

 

 

 

 

 

 

For example, System.Drawing.dll can be viewed logically as a binary DLL. Viewed logically, it is a collection of related types. ILDasm.exe is the ideal tool to discover the logical layout of an assembly

Assembly Types

The compiler for C# or linker for C++ allows you to determine the main entry point of an assembly. The following table shows the types of assemblies you can create:

Type C++ C#
GUI Define WinMain() and use System.Windows.Forms classes Use /t:winexe and define a class with a Main() method and use System.Windows.Forms classes
Console Define main() Use /t:exe and define a class with a main() method
NT Services Define WinMain() and use System.ServiceProcess.ServiceBase classes Use /t:winexe and System.ServiceProcess.ServiceBase classes
ASP.NET Apply the /LD switch and use System.Web.UI classes Use /t:library and use System.Web.UI classes
Web Services Apply the /LD switch and use System.Web.Services classes Use /t:library and use System.Web.WebServices classes
Library Apply the /LD switch Use /t:library 

Whatever you can do in C# you can do in C++. The difference it too support. C# is accepted by Windows Forms and Web Forms designers. C++ is not. In other words, you do not get rapid application development with C++. Also note that the entry point in C# is always main() regardless of whether the application is GUI or console based

Assembly Names

Assemblies - Code Reuse

Much like the good old DLL, an assembly contains code that can shared and reused by multiple applications. However, unlike the old DLLs, assemblies can be configured as private (the default behavior), which means that an assembly is intended to be used by a single application on a single machine. This will greatly simplify the versioning and deployment of applications.

With .NET assemblies, cross-language inheritance is possible. In other words, it is possible to reuse and extend types between languages.

Assemblies - Type Boundary

Assemblies are used to define a boundary for the types they contain. In .NET the identity of a given type is defined in part by the assembly in which it resides. Therefore, if two assemblies both define a class named MyClass, they are considered independent identities in the .NET universe.

Assemblies - Versionable, Self-Describing Entities

In the world of COM, the programmer in in charge of correctly versioning a binary. The main problem with this is that the programmer-defined techniques are not enforced by the runtime. Another major headache is that a given COM server does not list the set of the binaries that must be present for it to function correctly (I overcome this by using the Depends utility that comes with the Win32 SDK)

Under .NET, an assembly's manifest is the entity responsible for listing all internal and external dependencies. Each assembly also has a version identifier that applies to all types and resources contained within each module of the assembly. Using this version identifier, the runtime is able to load the correct assembly on behalf of the calling client. An assembly's version identifier is composed of two pieces: A friendly text named the informational version, and a numeric identifier termed the compatibility version. It takes the numerical form of nMajor.nMinor.nBuildNumber.nRevision.

Assemblies - Security Context

In the .NET universe, security measures are scoped at the assembly level. The security constraints defined by an assembly are listed in the assembly's manifest. For example, if AssemblyA wanted to use a class in AssemblyB, it is AssemblyB that determines whether to allow access or not.

Assemblies - Side-by-Side Execution

One of the biggest advantages of .NET assemblies is the fact that multiple versions of the same assembly can be loaded and understood by the runtime. It is also possible to configure which version should be loaded using application configuration files.

Single file Assembly - Test

<Code here for CSharp_SingleFileAssembly>

Cross-Language Inheritance

It is possible to create a type in a .NET-aware language, say C#, and extend it (i.e., derive from it) in another .NET aware language, say VB.NET (VB.NET supports the Inherits keyword) This allows an application to be broken down into discrete binary building blocks

Exploring the Manifest

The following is the manifest of the Library module from Single File Assembly - Test sections

/* The following two blocks of .assembly extern list the external assemblies required by this assembly (Librrry). The Library assembly therefore requires the mscorlib.dll and System.Windows.Forms.dll assemblies. The .publickeytoken means that the corresponding assembly has been configured as a shared assembly and is used to reference the strong name of the assembly. The .ver attribute lists the required version number. */
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 1:0:2411:0
}
.assembly extern System.Windows.Forms
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 1:0:2411:0
}

/* The .assembly tag marks the friendly name of this assembly. The .ver tag defines the compatibility version number of this assembly. Note that Library assembly does not define a .publickeytoken tag, given that this assembly has not been configured as a shared assembly.  */
.assembly Library
{
.custom instance void [mscorlib]System.Reflection.AssemblyKeyNameAttribute::.ctor(string) = ( 01 00 00 00 00 ) 
.custom instance void [mscorlib]System.Reflection.AssemblyKeyFileAttribute::.ctor(string) = ( 01 00 00 00 00 ) 
.custom instance void [mscorlib]System.Reflection.AssemblyDelaySignAttribute::.ctor(bool) = ( 01 00 00 00 00 ) 
.custom instance void [mscorlib]System.Reflection.AssemblyTrademarkAttribute::.ctor(string) = ( 01 00 00 00 00 ) 
.custom instance void [mscorlib]System.Reflection.AssemblyCopyrightAttribute::.ctor(string) = ( 01 00 00 00 00 ) 
.custom instance void [mscorlib]System.Reflection.AssemblyProductAttribute::.ctor(string) = ( 01 00 00 00 00 ) 
.custom instance void [mscorlib]System.Reflection.AssemblyCompanyAttribute::.ctor(string) = ( 01 00 00 00 00 ) 
.custom instance void [mscorlib]System.Reflection.AssemblyConfigurationAttribute::.ctor(string) = ( 01 00 00 00 00 ) 
.custom instance void [mscorlib]System.Reflection.AssemblyDescriptionAttribute::.ctor(string) = ( 01 00 00 00 00 ) 
.custom instance void [mscorlib]System.Reflection.AssemblyTitleAttribute::.ctor(string) = ( 01 00 00 00 00 ) 
// --- The following custom attribute is added automatically, do not uncomment -------
// .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::.ctor(bool,
// bool) = ( 01 00 01 01 00 00 ) 
.hash algorithm 0x00008004
.ver 1:0:714:37942
}

/* The one and only .module tag means that this is a single file assembly. */
.module Library.dll
// MVID: {9BB55578-9580-4FCD-A7DA-2BCF02BEA8B2}
.imagebase 0x11000000
.subsystem 0x00000003
.file alignment 512
.corflags 0x00000001
// Image base: 0x038c0000

 

Exploring Types

Make sure the following points are clear:

Understanding Private Assemblies

Formally speaking, an assembly can be either private or shared. Each variation provides the same underlying structure and the same kind of services. The difference between private and shared assemblies boils down to naming conventions, versioning policies, and deployment issues. Here are the main points concerning private assemblies:

Probing Basics

The .NET runtime resolves the location of a private assembly using a technique called probing. Probing is the process of mapping an external assembly reference to the correct corresponding binary file, and once an assembly is located, it is bound to the assembly that uses it

/* When the runtime reads the following line from an application manifest, a search is made in the application directory for a file named CarLibrary.DLL. If a DLL binary cannot be found, an attempt is make to locate an EXE version. If neither attempt is successful, an attempt is made to search for a shared assembly */
.assembly extern CarLibrary
{
...
}

Identity of a Private Assembly

The identity of a private assembly consists of a friendly string name and numerical version, both of which are recorded in the assembly's manifest. Be aware that it is possible for a single machine to have multiple copies of the same private assemblies in various application directories.

Private Assemblies and XML Configuration Files

When ,NET is instructed to bind to an assembly, the first step is to determine the presence of an application configuration file. These optional XML files contain XML tags that control the binding behavior of the launching application. Configuration files must have the same name as the application, have an extension of *.config, and be placed in the application directory with launching application. Configuration files can be used to specify an optional directories to be searched during the binding process.

.NET configuration files all have the same general format:

<configuration>
    <configSections>
        <!-- Description of each section -->
        ... 
    </configSections>

    <!-- Sections -->
</configuration>

A .NET application can be deployed simply by placing all assemblies in the application directory. Configuration files are used to when you want to deploy an application with a subdirectory tree, \bin, \help, \tools, \documentation, etc.. Configuration files instruct the runtime where it should probe while it is attempting to locate the set of private assemblies used by the launching application. 

<configuration>
    <runtime>
        <assemblyBinding xmlns='urn:schemas-microsoft-com:asm.v1'?
            <probing privatePath = 'Foo\Bar'/>
        </assemblyBinding>        
    </runtime>
</configuration>

Assemblies do not have to be located on the hard disk. They can be located on a remote server. In this case, the URI for the assembly can be specified in a <codeBase> element in the assemblies collection in the configuration file. The <codeBase> element  can also be used to load assemblies from a directory other than the application's directory and subdirectories, as long as the assembly has a strong name.

Understanding Shared Assemblies

Unlike private assemblies, a shared assembly can be used by several clients on a single machine. Shared assemblies are not typically placed in an application's directory, but are rather installed into a machine-wide Global Assembly Cache - GAC. The GAC is located under <dirve>:\WinNT\Assembly subdirectory. In COM shared DLL can reside anywhere as long as they are properly registered. In .NET, all shared assemblies must be in the GAC.

Unlike private assemblies, a shared assembly requires additional version information beyond the friendly text name. .NET runtime does enforce version checking on shared assemblies before loading on behalf of a calling application. In addition, a shared assembly must be assigned a shared name knows as the strong name.

Understanding Shared (Strong) Names

When creating a shared assembly, the first step is to create a unique shared name for the assembly. A shared name consists of the following information:

Assume a client has referenced a shared assembly, foo.dll. When the compiler generates the client' sbinary, the assembly's public key token is recorded in the client's manifest. At runtime, .NET runtime ensures that both client and shared assembly are using the same key pair. If these keys are identical, the client is assured that it is using the right assembly:

Building a Shared Assembly

A strong name is generated using the sn.exe utility with the -k argument. A strong name is created in two steps:

  1. Use sn.exe to create a new strong name key
        sn.exe -k MyFirstKey.snk   
  2. Record the public key in the assembly manifest. Note that a .NET C# project contains a file named AssemblyInfo.cs, which contains a number of empty attributes that are consumed by a .NET-aware compiler. Update the AssemblyKeyFile with a string specifying the location of the .snk file and rebuild:
        [assembly: AssemblyKeyFile( @"d:\SharedAssembly\MyFirstKey.snk") ]

Installing Shared Assemblies into the GAC

You must have administrative rights on the PC in order to install to the GAC. Simply drag and drop the shared assembly into the GAC directory. Alternatively you could also use gacutil.exe utility.

Using a Shared Assembly

  1. Create an assembly using the C# Class Library project. Add code and build
  2. Generate a string name using sn.exe.
  3. Record the public key name in the assembly's manifest.
  4. Install the shared assembly into the GAC
  5. Create a client application and set a reference to the shared assembly. Make sure the Copy Local project attribute for the referenced shared assembly is false.

Understanding .NET Version Policies 

Recall that .NET runtime does not bother to perform version checks for private assemblies. However, for a shared assembly, the version is of prime importance. 

A version number is composed of four parts:

The rules are as follows:

Recoding Version Information

Where do you specify version information? Recall that each C# project contains a file names AssemblyInfo.cs. In this file, there is an attribute names AssemblyVersion set to "1.0"

    [assembly: AssemblyVersion( "1.0.*" ) ]        // IDE automatically updates Revision and Build numbers
    [assembly: AssemblyVersion( "1.0.23.67" ) ]    // Revision and Build numbers updates by the programmer

It is your responsibility to update the version information. Be aware that the IDE automatically increments the Revision and Build numbers (as marked by the * tag). 

The Default Version Policy

The .NET default version policy behaves as follows:  If a client references a shared assembly, the major and minor versions must be identical for the bind to succeed. The .NET runtime binds to a given assembly if the assembly reference differs by the Revision or Build numbers.

For example, if a client's manifest explicitly requests version 1.0.0.0, but the GAC has newer version by specifying a QFE (say 1.0.0.3), the client automatically receives the newer version.

Specifying Custom Version Policies

If you want to customize how an application binds to an assembly (such as disabling QFEs), you need to author an application configuration file. Because .NET framework allows you to place multiple versions of the same shared assembly in the GAC, you can easily configure custom version policies as you see fie.

For example, you can specify a specific assembly version to be loaded, regardless of what may be listed in the manifest. This can be achieved using the <dependentAssembly> and <bindingRedirect> attributes:

<configuration>
    <runtime>
        <assemblyBinding xmlns='urn:schemas-microsoft-com:asm.v1'?
            <dependentAssembly>
                <assemblyIdentity name="sharedAssembly" publicKeyToken="..." culture=""/>
                <bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0" />               
            </dependentAssembly>
        </assemblyBinding>        
    </runtime>
</configuration>


Administrator Configuration File

The .NET framework also supports an administration configuration file. Each .NET-aware machine has a file named machine.config that contains listings used to override any application-specific configuration files. Reading this file is an excellent way to learn about *.config centric tags.

System.AppDomain and Threading

The process of building multithreaded applications in .NET has been simplified. Using types defined in the System.Threading namespace allows to spawn threads with relative ease. Likewise, there are types the provide the same synchronization primitives as in Win32 (mutexes, semaphores, etc.)

Recall that a .NET application is a collection of related assembly. A .NET process can host any number of application domains (AppDomains), each of which is fully and completely isolated from the other AppDomains. A .NET application (as opposed to a .NET process) runs in an AppDomain. Therefore, applications that run in different AppDomains are unable to share any information (global variables or static fields), unless they make use of the .NET remoting protocol. AppDomains are programmatically represented by the System.AppDomain type. System.AppDomain members provide numerous process-like behaviors.

AppDomain Test Project    <TO DO: Add a link to the test project>

Again, each host .NET process has at least one domain that contains threads executing IL code. You can create additional domains in the process but each one is isolated from the other. All IL is verified by the type system before it is executed to ensure that the correct code is called and that the code does not perform invalid memory operations. This verification is performed by the runtime, in addition to the memory protection applied at the process level by the OS. Thus the verification is applied at a sub process level within the domain.

Similar to IPC, communication between domains requires marshaling of data and object references through proxies. However, when the two domains that are communicating are within the same process, the mechanism is much faster than IPC. 

The analogy of domains being sub-processes within a process is useful. Application settings in .NET are applied at the domain level, so that if you have multiple domains in one process, they can be configured independently - each in a configuration file or by code.

When you load an assembly into a domain, the assembly is specific to that domain. It cannot directly access objects in any other domain, and thus it cannot access the memory used by other domains. If more than one domain loads the same assembly, a new copy of the assembly is loaded unless the assembly is loaded as domain-neutral. For example, mscorlib.dll assembly is domain-neutral. Domain-neutral assemblies can be private or shared assemblies. The point is that just one copy is loaded in each domain in a process.

Threads

Multithreaded programming is accomplished with the System.Threading namespace. In addition to providing types that represent a specific thread, the namespace also defines methods used to manage a collection of threads, a simple timer, and numerous types to provide synchronized access to data types.

Threading Project <TO DO: Add a link to the test project>

Note also that concurrency can be controlled with C# lock keyword, the System.Threading.Monitor class type, and the System.Threading.Interlocked type.