Application domains (also frequently called “app domains”) are critical to understanding assembly loading within the execution engine. They tend to be a bit mysterious and are often described in terms of their similarity to process address spaces, since they scope the visibility of components and resource handles, as well as provide a security and fault isolation barrier. But from our component model implementation point of view, they are not mysterious at all; application domains are the architectural elements that are responsible for loading and unloading assemblies into the execution engine. In addition, while assemblies are resident in memory, application domains provide for isolation on their behalf.
Although the isolation provided by application domains may bear some passing similarities to an operating system address space, they actually coexist within a single address space for a process. Because of this, all domains in a process share execution engine services such as the garbage collector. Application domains provide the means for externalizing references to their components, which means that their components can set up channels of communication between one another under a programmer’s control. Because component instances can pass such externalized references among themselves, threads of execution can traverse app domain boundaries; the execution engine carefully monitors these transitions to maintain isolation.
Assemblies are always loaded within the context of an app domain. All communication to and from external processes or components in other domains is mediated by the presence of a component’s domain; the execution engine has remoting and marshaling machinery that enforces isolation under the control of the app domain. When the cost of using this machinery is too high or when it is unnecessary, managed processes have the alternative of caching their assemblies in a domain that is reserved for the purpose of sharing assemblies. This is a special case, and it should be used only when necessary, since it compromises the protection afforded by domain isolation.
There are three well-known domains in every SSCLI process. The first is called the system domain, which is essentially a bootloader for types that are integral to the loading process, such as
System.Exception. The system domain loads and maintains a single assembly, named
mscorlib, which contains only trusted types and is not available for use for any other purpose. The system domain provides programmers with a way to root their searches for assemblies—there is a closure across all loaded types, which emanates from the system domain.
For nonsystem types that need to be shared, there is another special domain called the shared domain. Assemblies loaded in the shared domain are said to be domain-neutral, and their types are made directly available within every domain in the process. To be eligible for loading within this domain, an assembly must be strongly named and highly trusted. Advantages to being domain-neutral include resource savings in load time and memory consumption, and possibly lower marshaling costs. Note that not everything in the shared domain is shared; even when assemblies use it to cache their execution engine data structures and JIT-compiled code, individual domains still maintain private instances of the statics needed by the assembly’s types.
Normal types, such as your own unshared executables and shared libraries, load into a default domain. However, programmers may also choose to partition and isolate application-defined boundaries by creating their own application domains programmatically, either directly from managed code or else from unmanaged code hosting the execution engine. When multiple domains are used in this way, if a single type is loaded into more than one domain, each domain will contain an independent set of execution engine data structures to represent the type. This is necessary because the loading parameters may vary from domain to domain. Both class loaders and the security engine, which we will learn about in later chapters, are in cahoots with the implementation of app domains.
sscli/clr/src/vm for the implementation of
AppDomain and the two special domains,
SharedDomain. All three C++ classes share a common superclass named
BaseDomain, which implements many of their basic mechanisms.
One of the most important features of application domains is that they provide the only way to unload types (and the dependent resources of these types) from the execution engine. When a domain is unloaded, it carefully reclaims all of the resources associated with it before removing itself from service. A domain tracks both managed and unmanaged object instances and resources, and to clean these up and implement unloading, load activity for these entities must be carefully tracked and contained in the first place.
Despite all of these precautions, in some very special cases, it is both permissible and desirable to leak object state across app domain boundaries. Components that behave in this way are called
agile, since they can effectively move from domain to domain. Some important agile components
These are both common and have immutable state once loaded. This means that performance gains can be had by copying and caching their state across domains.
These are part of the execution engine infrastructure even though they are implemented as managed code. Security objects are backed by the global state of the execution engine itself, and because they can get to their state from within any domain, they qualify as agile.
These are very large, and duplicating them on a per-domain basis would be expensive, so they are implemented as agile.
These components must, by the nature of the service that they provide, be able to cross domain boundaries. They too are part of the execution engine infrastructure and are implemented as managed components.
The set of agile components is important but limited. They are often loaded into the system domain, since this domain can act as a home for trusted components that need to be available in every context. The complexities of implementing agile components, which include limits such as a strict ban on holding any references to non-agile components, restrict their representational possibilities.