Chapter 2. A Tour of the CLR Hosting API

The common language runtime (CLR) hosting API is a set of unmanaged functions and interfaces that a host uses to customize the CLR for its particular application model. Because the CLR has been designed to adapt to a variety of application scenarios, the amount of customization available through the hosting APIs is quite extensive. In most cases, you’ll find that your scenario requires only a subset of the total functionality provided by the API. The overview of the hosting API provided in this chapter is intended to give you an idea of the possible customization options so that you can decide which ones apply most to your application’s requirements.

Most of the individual features introduced here have entire chapters dedicated to them later in the book. This chapter merely provides the big picture. In addition to explaining the features, I’ll describe the design pattern used in the interfaces and provide enough background and samples to get you started using them.

The hosting API is defined in the file mscoree.idl, which can be found in the Include directory in the Microsoft .NET Framework software development kit (SDK). These programming interfaces consist of both unmanaged functions and a set of COM interfaces. The unmanaged functions are public exports from mscoree.dll. Most of these functions are used primarily for CLR initialization, but some are used to discover basic information about the CLR after it is running, such as which version was loaded and where the CLR installation resides on disk.

CorBindToRuntimeEx and ICLRRuntimeHost

The primary unmanaged function you’ll use is CorBindToRuntimeEx. This function is used to initialize the CLR into a process and is therefore the first of the hosting APIs you’re likely to call. One of the return parameters from CorBindToRuntimeEx is a pointer to an interface named ICLRRuntimeHost—the initial COM interface in the hosting API. I say "initial" because ICLRRuntimeHost is the first interface you’ll use when hosting the CLR. Given an interface pointer of type ICLRRuntimeHost, you gain access to all the other hosting functionality provided by the API. Figure 2-1 provides a sampling of the breadth of functionality a CLR host can access given a pointer to the ICLRRuntimeHost interface.

The CLR hosting interfaces as the gateway to the managed environment

Figure 2-1. The CLR hosting interfaces as the gateway to the managed environment

Because of its role as the initial interface that hosts use to customize the CLR, ICLRRuntimeHost plays a part in every CLR host you’ll write. As such, this interface will show up in one way or another in almost every chapter in this book. Table 2-1 provides an overview of the capabilities of ICLRRuntimeHost by briefly describing each method. The table also describes where in the book to look for more detail about each method.

Table 2-1. The Methods on ICLRRuntimeHost

Method

Description

Start

Starts the CLR running in a process. Details provided in Chapter 3.

Stop

Stops the CLR once it has been loaded into a process. Details provided in Chapter 3.

GetHostControl

Used by the CLR to discover which other interfaces the host implements. This method is described in more detail later in this chapter. See the "Hosting Manager Discovery" section.

GetCLRControl

Used by a CLR host to obtain a pointer to one of the hosting interfaces implemented by the CLR. This method is described in more detail later in this chapter. See the "Hosting Manager Discovery" section.

UnloadAppDomain

Unloads an application domain from the process. Details provided in Chapter 5.

GetCurrentAppDomainId

Returns the unique numerical identifier for the application domain in which the calling thread is currently running. I discuss this method and the overall role of application domain identifiers in Chapter 5.

ExecuteInDomain

Executes a callback function in a particular application domain.See Chapter 7 for details.

ExecuteApplication

Executes an application defined by a formal application manifest. Application manifests are a new concept in Microsoft .NET Framework 2.0. I don’t cover application manifests or the ExecuteApplication method at all in this book. See the Microsoft .NET Framework SDK for details.

ExecuteInDefaultAppDomain

Executes a given method in the default application domain. This method is handy for CLR hosts that use only one application domain. This method is discussed more in Chapter 7.

CLR Hosting Managers

The COM interfaces in the hosting API that the host and the CLR use to communicate with each other are grouped into what are called hosting managers. A hosting manager is nothing more than a convenient way to categorize a set of interfaces that work together to provide a logical grouping of functionality.

For example, a host can use the hosting interfaces to provide a set of allocation primitives through which the CLR will direct requests to allocate memory. This manager is referred to as the memory manager and consists of the interfaces IHostMemoryManager and IHostMalloc. As another example, consider the manager that lets a host control various aspects of how assemblies are loaded in a process. Four interfaces are involved in providing this customization: IHostAssemblyManager, IHostAssemblyStore, ICLRAssemblyIdentityManager, and ICLRAssemblyReferenceList. These interfaces are grouped into what is called the assembly loading manager.

Both of the managers in the preceding examples consist of multiple interfaces, as many managers are. In these cases, one of the interfaces is designated the primary interface. The main role of the primary interface (apart from implementing some of the functionality of the manager) is to participate in the discovery process, or the steps that are taken to identify which managers the host and the CLR implement. (More on this in the section entitled "Hosting Manager Discovery" later in this chapter.)

A few important points about the hosting managers are worth highlighting. First, some managers are implemented by the CLR, some are implemented by the host, and some are split, in that both the host and the CLR implement portions of the manager that complement each other. The hosting interfaces follow a naming convention that makes it easy to tell whether a particular interface is implemented by the CLR or by the host. All interfaces that are implemented by the CLR start with ICLR, whereas those implemented by the host start with IHost. Second, the factoring of the API into a set of managers gives the host the flexibility to customize some, but not all, aspects of the CLR. Every manager is optional; that is, the host implements only those managers that support the customizations they require. However, if you decide to implement a particular manager, you must implement all of it. You cannot implement just those methods you choose and delegate the rest to the CLR defaults, for example. The fact that hosts get to pick only those customizations that are specific to their needs is what makes the CLR so adaptable to a variety of scenarios.

Table 2-2 briefly describes all the managers in the CLR hosting API. (The primary interface for each manager is shown in boldface.)

Table 2-2. The CLR Hosting API Managers

Manager

Purpose

Interfaces in the Manager

assembly loading

Used by hosts to customize various aspects of the assembly loading process.

IHostAssemblyManager

IHostAssemblyStore

ICLRAssemblyReferenceList

ICLRAssemblyIdentityManager

host protection

Enables enforcement of a host-specific programming model.

ICLRHostProtectionManager

failure policy

Enables the host to customize the way failure conditions are handled by the CLR.

ICLRPolicyManager

IHostPolicyManager

memory

Enables the host to provide a set of allocation primitives through which the host will allocate memory. The CLR uses these primitives instead of their Microsoft Win32 equivalents.

IHostMemoryManager

IHostMalloc

ICLRMemoryNotificationCallback

threading

Enables the host to provide primitives to create and manipulate tasks (an abstract notion of a "unit of execution"). The CLR uses these primitives instead of their Win32 equivalents. The threading manager also enables a host to trap all calls into and out of the CLR.

IHostTaskManager

ICLRTaskManager

IHostTask

ICLRTask

thread pool manager

Enables the host to provide a custom implementation of the thread pool used by the CLR.

IHostThreadPoolManager

synchronization

Enables the host to provide a set of synchronization primitives such as events and semaphores. The CLR will use these primitives instead of their Win32 equivalents.

IHostSyncManager

ICLRSyncManager

IHostCriticalSection

IHostManualEvent

IHostAutoEvent

IHostSemaphore

I/O completion

Enables the host to plug in a custom implementation to handle overlapped input/output (I/O).

IHostIoCompletionManager

ICLRIoCompletionManager

garbage collection

Enables a host to force a collection, obtain statistics about recent collections, and receive notifications when collections begin and end. Unlike the other managers, the interfaces in the Garbage Collection Manager are discovered independently; hence, there is no primary interface.

IHostGCManager[1]

ICLRGCManager

debugging

Enables a host to customize how debugging works in its particular scenario.

ICLRDebugManager

CLR events

Enables a host to receive information about various events happening in the CLR, such as the unloading of an application domain.

ICLROnEventManager

IActionOnCLREvent[2]

[1] Neither of the interfaces in the garbage collection manager can be considered a primary interface. These two interfaces are independent and don’t need to be used together, so one isn’t obtained from the other.

[2] IActionOnCLREvent is implemented by the host. As such, it should probably have been named IHostActionOn-CLREvent to follow the naming convention that indicates whether the implementation for a given interface is provided by the CLR or by the host.

Given this background, let’s begin our tour of the hosting API by looking at how to initialize and load the CLR in a process.

CLR Initialization and Startup

As described, CorBindToRuntimeEx is the API you’ll call to initialize the CLR. I briefly describe the API in this section, and then we’ll dig into the details in Chapter 3.

There are four configuration settings you can specify when calling CorBindToRuntimeEx:

  • Version. You either can specify an exact version of the CLR to load, or you can default to the latest version installed on the machine.

  • Build type. The CLR comes in two flavors—a workstation build and a server build. As their names suggest, the workstation build is tuned for workstations, whereas the server build is tuned for the high-throughput scenarios associated with multiprocessor server machines.

  • Garbage collectionThe CLR garbage collector can run in either concurrent mode or nonconcurrent mode. The garbage collection mode is very closely tied to the type of build you select. Concurrent mode is used exclusively with the workstation build of the CLR because it’s tuned to work best with applications that have a high degree of user interactivity. Typically, nonconcurrent garbage collection is used with the server build of the CLR to support the high-throughput requirements of applications such as Web servers or database servers.

  • Domain-neutral code. The term domain neutral refers to the capability of sharing the jitcompiled code for an assembly across all application domains in a process. Much more is explained about domain-neutral code in Chapter 9.

Here’s a sample call to CorBindToRuntimeEx that demonstrates how you’d specify these configuration settings. In this call, I’ve specified that I want to run with only version 2.0.40103 of the CLR. In addition, I’d like to always run the workstation build with the garbage collector in the concurrent mode.

ICLRRuntimeHost *pCLRHost = NULL;
HRESULT hr = CorBindToRuntimeEx(
      L"v2.0.40103",
      L"wks",
      STARTUP_CONCURRENT_GC,
      CLSID_CLRRuntimeHost,
      IID_ICLRRuntimeHost,
      (PVOID*) &pCLRHost);

CorBindToRuntimeEx is implemented in mscoree.dll. mscoree.dll does not contain the implementation of the CLR engine, but rather is a shim whose primary job is to find and load the requested version of the CLR engine. As such, you’ll often hear mscoree.dll referred to as the CLR startup shim. The startup shim is required to support the side-by-side architecture of the CLR and the .NET Framework. Side-by-side refers to the ability to have multiple versions of the core CLR and the .NET Framework assemblies installed on a machine at the same time. As we see in Chapter 3, this architecture helps solve the "DLL hell" problems associated with platforms such as Win32 and COM. To keep multiple installations separate, each version of the CLR is installed into its own subdirectory under %windir%microsoft.netframework. (Some files are also stored in the global assembly cache.) The startup shim ties the multiple versions of the CLR together. Specifically, the shim tracks which versions are installed and is capable of finding the location on disk of a specific version of the CLR. Because of its role as arbitrator, the shim is not installed side by side. Each machine has only one copy of mscoree.dll installed in %windir%system32. All requests to load the CLR come through the startup shim, which then directs each request to the requested version of the CLR. Figure 2-2 shows the side-by-side architecture of the CLR and the .NET Framework. We cover this topic in much greater detail in Chapter 3.

The side-by-side architecture of the CLR and the .NET Framework

Figure 2-2. The side-by-side architecture of the CLR and the .NET Framework

A call to CorBindToRuntimeEx sets the version, garbage collection, build type, and domain-neutral parameters, but it does not actually start the CLR running in a process. My definition of start is that the version-specific CLR DLLs are loaded into the process and managed code is then ready to run. Starting the CLR occurs when the host calls the Start method on the ICLRRuntimeHost interface. You’ll notice in the preceding code that CorBindToRuntimeEx returns an interface pointer of type ICLRRuntimeHost as its last parameter as I discussed earlier. In this way, CorBindToRuntimeEx serves two roles as far as a host is concerned: it initializes the CLR and returns an interface pointer from which all interaction between the host and the CLR begins.

The combination of CorBindToRuntimeEx and ICLRRuntimeHost::Start gives you explicit control over several aspects of the CLR including some basic settings and the exact time at which the CLR is loaded. However, calling these functions isn’t strictly required to load the CLR into a process. If explicit calls to CorBindToRuntimeEx and Start are not made, the CLR will be loaded implicitly in certain scenarios. Although this can be convenient in some cases, implicit loading of the CLR removes your ability to configure it in the way I’ve been describing. One scenario in which the CLR is often started implicitly is when a managed type is created in a process through COM interoperability. If the CLR has not been initialized and started explicitly, the COM interoperability layer starts the runtime automatically to load and run the type. There might also be cases when you don’t want to load the CLR when the process starts, but you still want to configure some of the startup options. For example, you might want to lazily load the CLR to avoid having to pay the cost of starting the CLR when your process starts. You can use the hosting API LockClrVersion to register a callback that the CLR will call at the times when it would have loaded itself implicitly. This callback gives you the chance to call CorBindToRuntimeEx to initialize the CLR as you see fit. See Chapter 3 for a sample that uses LockClrVersion.

As we’ve seen, gaining control over CLR startup is easy—it takes just a few lines of code. Controlling startup in this way is just one small but useful example of the type of control you obtain through the hosting API with relatively little investment. Chapter 3 covers the use of CorBindToRuntimeEx and LockClrVersion in much more detail.

Other Unmanaged Functions on mscoree.dll

Although CorBindToRuntimeEx is the most commonly used export from mscoree.dll, it is probably not the only one you’ll ever use. Table 2-3 briefly describes some of the other commonly used exports that are of interest to hosts.

Table 2-3. Other Commonly Used Exports from mscoree.dll

API Name

Description

GetCORVersion

Returns the version of the CLR that is loaded in the process. GetCORVersion returns null if called before the CLR is started.

GetCORSystemDirectory

Returns the directory on disk in which the loaded CLR is installed (see Figure 2-2). This function is useful for locating compilers and others tools that are located in the CLR installation directory.

LockClrVersion

Enables you to register a callback so you can lazily control CLR initialization.

GetRequestedRuntimeInfo

Enables you to determine whether a given version of the CLR is present on the machine.

Hosting Manager Discovery

The COM interfaces in the hosting API are factored into a set of pluggable managers, as described earlier. This factoring allows a host to implement only the managers of interest in a particular scenario. The fact that a host can optionally provide a particular manager means the hosting interfaces must include a protocol by which the CLR can determine which, if any, managers a host chooses to implement. Remember, too, that some managers are implemented completely on the CLR side, such as the host protection manager used to enforce host-specific programming model considerations. These managers are always provided by the CLR if a host asks for them. As such, there must also be a mechanism for a host to ask for a particular CLRimplemented manager.

The discovery of the managers supported by a host and the managers supplied by the CLR are arbitrated through two interfaces named IHostControl and ICLRControl. As their names suggest, IHostControl is implemented by the host and ICLRControl is implemented by the CLR.

IHostControl contains a method called GetHostManager that the CLR uses to determine which managers a host supports, whereas ICLRControl contains a method called GetCLRManager that the host calls to obtain an interface pointer to one of the CLR managers.

Figure 2-3 shows the relationship between the host, the CLR, the control interfaces, and the hosting managers. For those managers in which the implementation is split between the CLR and the host, the primary manager interface is always provided by the host.

IHostControl, ICLRControl, and the hosting managers

Figure 2-3. IHostControl, ICLRControl, and the hosting managers

Discovering Host-Implemented Managers

Now that we’ve looked at all the pieces, let’s take a look at how the CLR determines which managers a host implements.

Step 1: The Host Supplies a Host Control Class

After the host’s call to CorBindToRuntimeEx returns, the host initializes an instance of its host control class (that is, a class derived from IHostControl) and informs the CLR of its existence by calling ICLRRuntimeHost::SetHostControl. It’s important to note that the host must do this before ICLRRuntimeHost::Start is called—calls to SetHostControl that occur after Start have no effect.

Given a host-implemented control class such as the following:

class CHostControl : public IHostControl
{
public:
   // Methods from IHostControl
   HRESULT __stdcall GetHostManager(REFIID riid,
                           void **ppObject);

   // Additional methods, including those from IUnknown, omitted for clarity
};

the code in the host to set the host control object looks like this:

ICLRRuntimeHost *pCLR = NULL;
// Initialize the clr
HRESULT hr = CorBindToRuntimeEx(L"v2.0.40103",
                                L"wks",
                                STARTUP_CONCURRENT_GC,
                                CLSID_CLRRuntimeHost,
                                IID_ICLRRuntimeHost,
                               (PVOID*) &pCLR);

// Initialize a new instance of our host control object
CHostControl *pHostControl = new CHostControl();

// Tell the CLR about it...
pCLR->SetHostControl((IHostControl *)pHostControl);

Step 2: The CLR Queries the Host Control Class

After CorBindToRuntimeEx returns, the CLR begins the process of setting itself up to run managed code. As part of this initialization, the CLR checks to see whether a host control has been registered (that is, the host called ICLRRuntimeHost::SetHostControl). If not, the host does not implement any managers as far as the CLR is concerned, and initialization proceeds. If a host control class has been registered, the CLR will call IHostControl::GetHostManager once for every manager for which the primary interface is implemented by the host. Specifically, GetHostManager will be called once for the following IIDs:

  • IID_IHostMemoryManager

  • IID_IHostTaskManager

  • IID_IHostThreadPoolManager

  • IIE_IHostIoCompletionManager

  • IID_IHostSyncManager

  • IID_IHostAssemblyManager

  • IID_IHostGCManager

  • IID_IHostPolicyManager

If a valid interface pointer is returned for a given IID, the host is said to implement that manager. If E_NOINTERFACE is returned, the host does not implement that manager.

Here’s a sample host’s implementation of IHostControl::GetHostManager that supports just the memory manager and the assembly loading manager:

HRESULT __stdcall CHostControl::GetHostManager(REFIID riid, void **ppObject)
{
   if (riid == IID_IHostMemoryManager)
   {
      // The CLR is asking for a memory manager. Create an instance of the
      // host's memory manager and return it. Note: This snippet assumes
      // the host's implementation of the memory manager is in
      // a class named CHostMemoryManager
      CHostMemoryManager *pMemManager = new CHostMemoryManager();
         *ppObject = (IHostMemoryManager *)pMemManager;
          return S_OK;
   }
   else if (riid == IID_IHostAssemblyManager)
   {
         // The CLR is asking for an assembly loading manager.  Create an
         // instance of the host's manager and return it. Note: This
         // snippet assumes the host's implementation of the Assembly Loading
         // Manager is in a class named CHostAssemblyManager
         CHostAssemblyManager *pAsmManager = new CHostAssemblyManager();
         *ppObject = (IHostAssemblyManager *)pAssemblyManager;
         return S_OK;
   }
   // This host doesn't support any other managers – so just return
   // E_NOINTERFACE
   *ppObject = NULL;
   return E_NOINTERFACE;
}

After the series of calls to GetHostManager, the CLR has a complete picture of which managers the host supports. From this point on, the CLR’s interaction with the managers goes directly through each manager’s primary interface. The host control object’s only job is to return managers to the CLR. After that, it’s out of the picture.

Obtaining CLR-Implemented Managers

As described earlier, the CLR provides a set of managers that hosts can use to customize various aspects of a running CLR. These managers are implemented completely on the CLR side. A host’s interaction with these managers is very simple because the communication is all one way—from the host to the CLR.

The process a host uses to obtain interface pointers to the CLR-implemented managers is relatively straightforward. First, the host obtains an interface pointer to an ICLRControl by calling ICLRRuntimeHost::GetCLRControl. Next, the host calls ICLRControl::GetCLRManager, passing in the IID of the interface corresponding to the manager of interest. For example, the following code obtains a pointer to the failure policy manager, which is used to customize the way the CLR behaves in the face of resource failures:

// Get a pointer to an ICLRControl (pCLR is of type ICLRRuntimeHost *)
ICLRControl *pCLRControl = NULL;
pCLR->GetCLRControl(&pCLRControl);

// Ask for the Failure Policy Manager
ICLRPolicyManager *pPolicy = NULL;
pCLRControl->GetCLRManager(IID_ICLRPolicyManager, (void **)&pPolicy);

Once the desired interface pointer is returned, the host saves it and calls the methods relevant to the customization it desires. The semantics of when each of the managers can be called and which settings can be configured varies by manager. The rest of this chapter provides an overview of each of the major hosting managers. Details about how to use these managers are provided in subsequent chapters in this book.

Overview of the Hosting Managers

Earlier in the chapter, I described the manager architecture, listed each manager and its interfaces, and talked about how the CLR and the host go about obtaining manager implementations. In this section, I take a brief look at each manager to understand how it can be used by an application to customize a running CLR.

Assembly Loading

The CLR has a default, well-defined set of steps it follows to resolve a reference to an assembly. These steps include applying various levels of version policy, searching the global assembly cache (GAC), and looking for the assemblies in subdirectories under the application’s root directory. These defaults include the assumption that the desired assembly is stored in a binary file in the file system.

These resolution steps work well for many application scenarios, but there are situations in which a different approach is required. Remember that CLR hosts essentially define a new application model. As such, it’s highly likely that different application models will have different requirements for versioning, assembly storage, and assembly retrieval. To that end, the assembly loading manager enables a host to customize completely the assembly loading process. The level of customization that’s possible is so extensive that a host can implement its own custom assembly loading mechanism and bypass the CLR defaults altogether if desired.

Specifically, a host can customize the following:

  • The location from which an assembly is loaded

  • How (or if) version policy is applied

  • The format from which an assembly is loaded (assemblies need not be stored in standalone disk files anymore)

Let’s take a look at how Microsoft SQL Server 2005 uses the assembly loading manager to get an idea of how these capabilities can be used.

As background, SQL Server 2005 allows user-defined types, procedures, functions, triggers, and so on to be written in managed languages. A few characteristics of the SQL Server 2005 environment point to the need for customized assembly binding:

  • Assemblies are stored in the database, not in the file system. Managed code that implements user-defined types, procedures, and the like is compiled into assemblies just as you’d expect, but the assembly must be registered in SQL before it can be used. This registration process physically copies the contents of the assembly into the database. This self-contained nature of database applications makes them easy to replicate from server to server.

  • The assemblies installed in SQL are the exact ones that must be run. SQL Server 2005 applications typically have very strict versioning requirements because of the heavy reliance on persisted data. For example, the return value from a managed user-defined function might be used to build an index used to optimize performance. It is imperative that only the exact assembly that was used to build the index is used when the application is run. If a reference to that assembly were somehow redirected through version policy, the index that was previously stored could become invalid.

To support these requirements, SQL Server 2005 makes extensive use of the assembly loading manager to load assemblies out of the database instead of from the file system and to bypass many of the versioning rules that the CLR follows by default.

It’s important to notice, however, that not all assemblies are stored and loaded out of the database by SQL. The assemblies used in a SQL Server 2005 application fall into one of two categories: the assemblies written by customers that define the actual behavior of the application (the add-ins), and the assemblies written by Microsoft that ship as part of the Microsoft .NET Framework. In the SQL case, only the add-ins are stored in the database—the Microsoft .NET Framework assemblies are installed and loaded out of the global assembly cache.

In fact, it is often the case that a host will want to load only the add-ins in a custom fashion and let the default CLR behavior govern how the Microsoft .NET Framework assemblies are loaded. To support this idea, the assembly loading manager enables the host to pass in a list of assemblies that should be loaded in the normal, default CLR fashion. All other assembly references are directed to the host for resolution.

Those assemblies that the host resolves can be loaded from any location in any format. These assemblies are returned from the host to the CLR in the form of a pointer to an IStream interface. For hosts that implement the assembly loading manager (that is, provide an implementation of IHostAssemblyManager when queried for it through IHostControl::GetHostManager), the process of binding generally works like this:

  1. As the CLR is running code, it often finds references to other assemblies that must be resolved for the program to run properly. These references can be either static in the calling assembly’s metadata or dynamic in the form of a call to Assembly.Load or one of the other class library methods used to load assemblies.

  2. The CLR looks to see if the reference is to an assembly that the host has told the CLR to bind to itself (a Microsoft .NET Framework assembly in our SQL example). If so, binding proceeds as normal: version policy is applied, the global assembly cache is searched, and so on.

  3. If the reference is not in the list of CLR-bound assemblies, the CLR calls through the interfaces in the Assembly Manager (IHostAssemblyStore, specifically) to resolve the assembly.

  4. At this point, the host is free to load the assembly in any way and returns an IStream * representing the assembly to the CLR. In the SQL scenario, the assembly is loaded directly from the database.

Figure 2-4 shows the distinction between how add-ins and the Microsoft .NET Framework assemblies are loaded in SQL Server 2005.

Assembly loading in the SQL Server 2005 host

Figure 2-4. Assembly loading in the SQL Server 2005 host

Details of how to implement an assembly loading manager to achieve the customizations described here is provided in Chapter 8.

Customizing Failure Behavior

The CLR hosting APIs are built to accommodate a variety of hosts, many of which will have different tolerances for handling failures that occur while running managed code in the process. For example, hosts with largely stateless programming models, such as ASP.NET, can use a process recycling model to reclaim processes deemed unstable. In contrast, hosts such as SQL Server 2005 and the Microsoft Windows shell rely on the process being stable for a logically infinite amount of time.

The CLR supports these different reliability needs through an infrastructure that can keep a single application domain or an entire process consistent in the face of various situations that would typically compromise stability. Examples of these situations include a thread that fails to abort properly (because of a finalizer that loops infinitely, for example) and the inability to allocate a resource such as memory.

In general, the CLR’s philosophy is to throw exceptions on resource failures and thread aborts. However, there are cases in which a host might want to override these defaults. For example, consider the case in which a failure to allocate memory occurs in a region of code that might be sharing state across threads. Because such a failure can leave the domain in an inconsistent state, the host might choose to unload the entire domain instead of aborting just the thread from which the failed allocation occurred. Although this action clearly affects all code running in the domain, it guarantees that the rest of the domains remain consistent and the process remains stable. In contrast, a different host might be willing to allow the questionable domain to keep running and instead will stop sending new requests into it and will unload the domain later.

Hosts use the failure policy manager to specify which actions to take in these situations. The failure policy manager enables the host to set timeout values for actions such as aborting a thread or unloading an application domain and to provide policy statements that govern the behavior when a request for a resource cannot be granted or when a given timeout expires. For example, a host can provide policy that causes the CLR to unload an application domain in the face of certain failures to guarantee the continued stability of the process as described in the previous example.

The CLR’s infrastructure for supporting scenarios requiring high availability requires that managed code library authors follow a set of programming guidelines aimed at proper resource management. These guidelines, combined with the infrastructure that supports them, are both needed for the CLR to guarantee the stability of a process. Chapter 11 discusses how hosts can customize CLR behavior in the face of failures and also describes the coding guidelines that library authors must follow to enable the CLR’s reliability guarantees.

Programming Model Enforcement

The .NET Framework class libraries provide an extensive set of built-in functionality that hosted add-ins can take advantage of. In addition, numerous third-party class libraries exist that provide everything from statistical and math libraries to libraries of new user interface (UI) controls.

However, the full extent of functionality provided by the set of available class libraries might not be appropriate in particular hosting scenarios. For example, displaying user interface in server programs or services is not useful, or allowing add-ins to exit the process cannot be allowed in hosts that require long process lifetimes.

The host protection manager provides the host with a means to block classes, methods, properties, and fields offering a particular category of functionality from being loaded, and therefore used, in the process. A host can choose to prevent the loading of a class or the calling of a method for a number of reasons including reliability and scalability concerns or because the functionality doesn’t make sense in that host’s environment, as in the examples described earlier.

You might be thinking that host protection sounds a lot like a security feature, and in fact we typically think of disallowing functionality to prevent security exploits. However, host protection is not about security. Instead, it’s about blocking functionality that doesn’t make sense in a given host’s programming model. For example, you might choose to use host protection to prevent add-ins from obtaining synchronization primitives used to coordinate access to a resource from multiple threads because taking such a lock can limit scalability in a server application. The ability to request access to a synchronization primitive is a programming model concern, not a security issue.

When using the host protection manager to disallow certain functionality, hosts indicate which general categories of functionality they’re blocking rather than individual classes or members. The classes and members contained in the .NET Framework class libraries are grouped into categories based on the functionality they provide. These categories include the following:

  • Shared state. Library code that exposes a means for add-ins to share state across threads or application domains. The methods in the System.Threading namespace that allow you to manipulate the data slots on a thread, such as Thread.AllocateDataSlot, are examples of methods that can be used to share state across threads.

  • Synchronization. Classes or members that expose a way for add-in to hold locks. The Monitor class in the System.Threading namespace is a good example of a class you can use to hold a lock.

  • ThreadingAny functionality that affects the lifetime of a thread in the process. Because it causes a new thread to start running, System.Threading.Thread.Start is an example of a method that affects thread lifetime within a process.

  • Process management. Any code that provides the capability to manipulate a process, whether it be the host’s process or any other process on the machine. System.Diagnostics.Process.Start is clearly a method in this category.

Classes and members in the .NET Framework that have functionality belonging to one or more of these categories are marked with a custom attribute called the HostProtectionAttribute that indicates the functionality that is exposed. The host protection manager comes into play by providing an interface (ICLRHostProtectionManager) that hosts use to indicate which categories of functionality they’d like to prevent from being used in the process. The attribute settings in the code and the host protection settings passed in through the host are examined at runtime to determine whether a particular member is allowed to run. If a particular member is marked as being part of the threading category, for example, and the host has indicated that all threading functionality should be blocked, an exception will be thrown instead of the member being called.

Annotating code with the category custom attributes and using the host protection manager to block categories of functionality is described in detail in Chapter 12.

Memory and Garbage Collection

The managers we’ve looked at so far have allowed the host to customize different aspects of the CLR. Another set of managers has a slightly different flavor—these managers enable a host to integrate its runtime environment deeply with the CLR’s execution engine. In a sense, these managers can be considered abstractions over the set of primitives or resources that the CLR typically gets from the operating system (OS) on which it is running. More generally, the COM interfaces that are part of the hosting API can be viewed as an abstraction layer that sits between the CLR and the operating system, as shown in Figure 2-5. Hosts use these interfaces to provide the CLR with primitives to allocate and manage memory, create and manipulate threads, perform synchronization, and so on. When one of these managers is provided by a host, the CLR will use the manager instead of the underlying operating system API to get the resource. By providing implementations that abstract the corresponding operating system concepts, a host can have an extremely detailed level of control over how the CLR behaves in a process. A host can decide when to fail a memory allocation requested by the CLR, it can dictate how managed code gets scheduled within the process, and so on.

The hosting APIs as an abstraction over the operating system

Figure 2-5. The hosting APIs as an abstraction over the operating system

The first manager of this sort that we examine is the memory manager. The memory manager consists of three interfaces: IHostMemoryManager, IHostMalloc, and ICLRMemoryNotificationCallback. The methods of these interfaces enable the host to provide abstractions for the following:

  • Win32 and the standard C runtime memory allocation primitives. Providing abstractions over APIs such as VirtualAlloc, VirtualFree, VirtualQuery, malloc, and free allow a host to track and control the memory used by the CLR. A typical use of the memory manager is to restrict the amount of memory the CLR can use within a process and to fail allocations when it makes sense in a host-specific scenario. For example, SQL Server 2005 operates within a configurable amount of memory. Oftentimes, SQL is configured to use all of the physical memory on the machine. To maximize performance, SQL tracks all memory allocations and ensures that paging never occurs. SQL would rather fail a memory allocation than page to disk. To track all allocations made within the process accurately, the SQL host must be able to record all allocations made by the CLR. When the amount of memory used is reaching the preconfigured limit, SQL must start failing memory allocation requests, including those that come from the CLR. The consequence of failing a particular CLR request varies with the point in time in which that request is made. In the least destructive case, the CLR might need to abort the thread on which an allocation is made if it cannot be satisfied. In more severe cases, the current application domain or even the entire process must be unloaded. Each request for additional memory made by the CLR includes an indication of what the consequences of failing that allocation are. This gives the host some room to decide which allocations it can tolerate failing and which it would rather satisfy at the expense of some other alternative for pruning memory.

  • The low-memory notification available on Microsoft Windows XP and later versionsWindows XP provides memory notification events so applications can adjust the amount of memory they use based on the amount of available memory as reported by the operating system. (See the CreateMemoryResourceNotification API in the Platform SDK for background.) The memory management interfaces provided by the CLR hosting API enable a host to provide a similar mechanism that allows a host to notify the CLR of low- (or high-) memory conditions based on a host-specific notion, rather than the default operating system notion. Although the mechanism provided by the operating system is available only on Windows XP and later versions, the notification provided in the hosting API works on all platforms on which the CLR is supported. The CLR takes this notification as a heuristic that garbage collection is necessary. In this way, hosts can use this notification to encourage the CLR to do a collection to free memory so more memory is made available from which to satisfy additional allocation requests.

In addition to the memory manager, the CLR hosting API also provides a garbage collection manager that allows you to monitor and influence how the garbage collector uses memory in the process. Specifically, the garbage collection manager includes interfaces that enable you to determine when collections begin and end and to initiate collections yourself.

We discuss the details of implementing both the memory and garbage collection managers in Chapter 13.

Threading and Synchronization

The most intricate of the managers provided in the hosting APIs are the threading manager and the synchronization manager. Although the managers are defined separately in the API, it’s hard to imagine a scenario in which a host would provide an implementation of the threading manager without implementing the synchronization manager as well. These managers work together to enable the host to customize the way managed code gets scheduled to run within a process.

The purpose of these two managers is to enable the host to abstract the notion of a unit of execution. The first two versions of the CLR assumed a world based on physical threads that were preemptively scheduled by the operating system. In .NET Framework 2.0, the threading manager and synchronization manager allow the CLR to run in environments that use cooperatively scheduled fibers instead. The threading manager introduces the term task as this abstract notion of a unit of execution. The host then maps the notion of a task to either a physical operating system thread or a host-scheduled fiber.

The scenarios in which these managers are used extensively are likely to be few, so I don’t spend too much time discussing them in this book. However, the subject is interesting if for no other reason than the insight it provides into the inner workings of the CLR.

The set of capabilities provided by the threading manager is quite extensive—enough to model a major portion of an operating system thread API such as Win32, with additional features specifically required by the CLR. These additional features include a means for the CLR to notify the host of times in which thread affinity is required and callbacks into the CLR so it can know when a managed task gets scheduled (or unscheduled), among others.

The general capabilities of the threading manager are as follows:

  • Task management. Starting and stopping tasks as well as standard operations such as join, sleep, alert, and priority adjustment.

  • Scheduling. Notifications to the CLR that a managed task has been moved to or from a runnable state. When a task is scheduled, the CLR is told which physical operating system thread the task is put on.

  • Thread affinity. A means for the CLR to tell the host of specific window during which thread affinity must be maintained. That is, a time during which a task must remain running and must stay on the current thread.

  • Delayed abort. There are windows of time in which the CLR is not in a position to abort a task. The CLR calls the host just before and just after one of these windows.

  • Locale management. Some hosts provide native APIs for users to change or retrieve the current thread locale setting. The managed libraries also provide such APIs (see System.Globalization.CurrentCulture and CurrentUICulture in the Microsoft .NET Framework SDK). In these scenarios, the host and the CLR must inform each other of locale changes so that both sides stay synchronized.

  • Task pooling. Hosts can reuse or pool the CLR-implemented portion of a task to optimize performance.

  • Enter and leave notifications. Hosts are notified each time execution leaves the CLR and each time it returns. These hooks are called whenever managed code issues a PInvoke or Com Interoperability call or when unmanaged code calls into managed code.

One feature that perhaps needs more explanation is the ability to hook calls between managed and unmanaged code. On the surface it might not be obvious how this is related to threading, but it ends up that hosts that implement cooperatively scheduled environments often must change how the thread that is involved in the transition can be scheduled.

Consider the scenario in which an add-in uses PInvoke to call an unmanaged DLL that the host knows nothing about. Because of the information received by implementing the threading and synchronization abstractions, the host can cooperatively schedule tasks running managed code just fine. However, when control leaves that managed code and enters the unmanaged DLL, the host no longer can know what that code is going to do. The unmanaged DLL could include code that takes a lock on a thread and holds it for long periods of time, for example. In this case, managed code should not be cooperatively scheduled on that thread because the host cannot control when it will next get a chance to run. This is where the hooks come in. When a host receives the notification that control is leaving the CLR, it can switch the scheduling mode of that thread from the host-control cooperative scheduling mode to the preemptive scheduling mode provided by the operating system. Said another way, the host gives responsibility for scheduling code on that thread back to the operating system. At some later point in time, the PInvoke call in our sample completes and returns to managed code. At this point, the hook is called again and the host can switch the scheduling mode back to its own cooperatively scheduled state.

I mentioned earlier that the threading manager and synchronization manager are closely related. The preceding example provides some hints as to why. The interfaces in the threading manager provide the means for the host to control many aspects of how managed tasks are run. However, the interfaces in the synchronization manager provide the host with information about how the tasks are actually behaving. Specifically, the synchronization manager provides a number of interfaces the CLR will use to create synchronization primitives (locks) when requested (or needed for internal reasons) during the execution of managed code. Knowing when locks are taken is useful information to have during scheduling. For example, when code blocks on a lock, it’s likely a good time to pull that fiber off a thread and schedule another one that’s ready to run. Knowing about locks helps a host tune its scheduler for maximum throughput.

There’s another scenario in which it’s useful for a host to be aware of the locks held by managed tasks: deadlock detection. It’s quite possible that a host can be running managed tasks and tasks written in native code simultaneously. In this case, the CLR doesn’t have enough information to resolve all deadlocks even if it tried to implement such a feature. Instead, the burden of detecting and resolving deadlocks must be on the host. Making the host aware of managed locks is essential for a complete deadlock detection mechanism.

Primarily for these reasons, the synchronization manager contains interfaces that provide the CLR with implementations of the following:

  • Critical sections

  • Events (both manual and auto-reset)

  • Semaphores

  • Reader/writer locks

  • Monitors

We dig into more details of these two managers in Chapter 14.

Other Hosting API Features

We’ve now covered most of the significant functionality the CLR makes available to hosts through the hosting API. However, a few more features are worth a brief look. These features are discussed in the following sections.

Loading Code Domain Neutral

When assemblies are loaded domain neutral, their jit-compiled code and some internal CLR data structures are shared among all the application domains in the process. The goal of this feature is to reduce the working set. Hosts use the hosting interfaces (specifically, IHostControl) to provide a specific list of assemblies they’d like to have loaded in this fashion. Although domain-neutral loading requires less memory, it does place some additional restrictions on the assembly. Specifically, the code that is generated is slightly slower in some scenarios, and a domain-neutral assembly cannot be unloaded until the process exits. As such, hosts typically do not load all assemblies domain neutral. In practice, the set of assemblies loaded in this way often are the system assemblies—add-ins are almost never loaded domain neutral so they can be dynamically unloaded while the process is running. This is the exact model that hosts such as SQL Server 2005 follow. Domain-neutral code is covered in detail in Chapter 9.

Thread Pool Management

Hosts can provide the CLR with a thread pool by implementing the thread pool manager. The thread pool manager has one interface (IHostThreadPoolManager) and provides all the functionality you’d expect including the capability to queue work items to the thread pool and set the number of threads in the pool. The thread pool manager is described in detail in Chapter 14.

I/O Completion Management

Overlapped I/O can also be abstracted by the host using the I/O completion manager. This manager enables the CLR to initiate asynchronous I/O through the host and receive notifications when it is complete. For more information on the I/O completion manager, see the documentation for the IHostIoCompletionPort and ICLRIoCompletionPort interfaces in the .NET Framework SDK.

Debugging Services Management

The debugging manager provides some basic capabilities that enable a host to customize the way debuggers work when attached to the host’s process. For example, hosts can use this manager to cause the debugger to group related debugging tasks together and to load files containing extra debugging information. For more information on the debugging manager, see the ICLRDebugManager documentation in the .NET Framework SDK.

Application Domain Management

Application domains serve two primary purposes as far as a host is concerned. First, hosts use application domains to isolate groups of assemblies within a process. In many cases, application domains provide the same level of isolation for managed code as operating system processes do for unmanaged code. The second common use of application domains is to unload code from a process dynamically. Once an assembly has been loaded into a process, it cannot be unloaded individually. The only way to remove it from memory is to unload the application domain the assembly was in. Application domains are always created in managed code using the System.AppDomain class. However, the hosting interface ICLRRuntimeHost enables you to register an application domain manager[1] that gets called by the CLR each time an application domain is created. You can use your application domain manager to configure the domains that are created in the process. In addition, ICLRRuntimeHost also includes a method that enables you to cause an application domain to be unloaded from your unmanaged hosting code.

Application domains are such a central concept to hosts and other extensible applications that I dedicate two chapters to them. The first chapter (Chapter 5) provides an overview of application domains and provides guidelines to help you use them most effectively. The second chapter (Chapter 6) describes the various ways you can customize application domains to fit your application’s requirements most closely.

CLR Event Handling

Hosts can register a callback with the CLR that gets called when various events happen when running managed code. Through this callback, hosts can receive notification when the CLR has been disabled in the process (that is, it can no longer run managed code) or when application domains are unloaded. More details on the CLR event manager are provided in Chapter 5.

Summary

The set of APIs described in this chapter allow the CLR to be customized to work in a variety of application environments. The extent of the customization allowed ranges from configuring basic startup parameters to controlling critical runtime notions such as how code is loaded into the process, how memory is managed, and when code is scheduled to run. The hosting API is factored into a set of managers that group logically related interfaces together. As the author of a CLR host, you get to choose which of these managers you’d like to implement so you can customize only those aspects of the CLR that are most important to your scenario.

This chapter provides an overview of the hosting API to give you an idea of the various ways the CLR can be customized. Throughout the rest of this book, we dig into different parts of the API in greater detail.



[1] The term "manager" as used here can be a bit confusing given the context in which we’ve used it in the rest of the chapter. An application domain manager isn’t a "manager" as specifically defined by the CLR hosting interfaces. Instead, it is a managed class that you implement to customize how application domains are used within a process.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.21.188