CLR Inside Out

CLR Hosting APIs

Alessandro Catorcini and Piotr Puszkiewicz

Contents

Hosting Managers
Memory Manager
Threading Manager
Synchronization Manager
I/O Completion Manager
Assembly Load Manager
CLR Configuration Manager
Using the Hosting APIs
Conclusion

Suppose you are developing a large application in native C++ and you want to allow your customers to extend this app so they can mold it to their needs. Allowing your customers to write the extensions in managed code inside the Microsoft® .NET Framework would make their development experiences much smoother than if they had to work with native code, but loading the common language runtime (CLR) into your application's process can be worrisome. What if an uncaught CLR exception ends up killing the application's process? What if the managed extensions used more memory than your application was willing to give up? You could solve these problems by launching the runtime in another process, but moving large amounts of data across process boundaries could soon become prohibitively expensive. Is there any way to take advantage of the benefits of managed code without such expenses?

These issues quite frequently arise in the development of extensible applications. The CLR provides a variety of functionality, resources, and libraries to reduce the cost of building extensions. However, developers of such extensible applications are understandably wary of allowing potentially unreliable and resource-hungry third-party code to execute inside of their applications. Fortunately, the CLR 2.0 hosting APIs allow application developers to control the CLR's resource consumption and to guarantee a specific level of reliability for code running within the CLR. By using the hosting APIs, developers of native hosts can execute managed code in-process with complete knowledge and control over how the CLR behavior can affect their application.

The CLR has always allowed a level of integration between itself and a host. In the .NET Framework 1.x, native applications were able to load a specific version of the CLR, to start and stop its execution, and to configure a limited number of settings. In version 2.0 of the Framework, the CLR allows for a much deeper integration. The updated hosting APIs provide layers of abstraction that let the host manage many of the resources currently provided by Win32®. Furthermore, the updated APIs extend the set of CLR functionality that is configurable by the host.

Hosting Managers

The CLR 2.0 hosting APIs are split into two primary sets: host managers and CLR managers. Host manager interfaces have names prefixed with IHost, and follow with a function description (as in IHostMemoryManager and IHostSecurityManager). The implementations of the host manager interfaces are provided by the developer of the host and are registered with the CLR, which uses these interfaces to make callbacks against the host for the duration of the process lifetime.

The CLR manager interfaces are prefixed with ICLR, and what follows ICLR in the name describes the function of the manager (as in ICLRTaskManager and ICLRSyncManager). CLR managers are handed from the CLR to the host and are implemented by the CLR. The host uses these interfaces to request that the CLR perform some specific action (for example, a host can use the provided ICLRGCManager to force a garbage collection).

For the purposes of this article, we'll group the CLR managers and host managers according to their intended uses, which will help clarify the key benefits of using the hosting APIs. We will include a list of the associated ICLR* and IHost* interfaces and their descriptions when covering an area of functionality.

Memory Manager

The memory manager lets the host provide an interface through which the CLR will request all memory allocations. It replaces both the Windows® memory APIs and the standard C CLR allocation routines. Moreover, the interface allows the CLR to inform the host of the consequences of failing a particular allocation (for example, failing a memory allocation from a thread holding a lock may have certain reliability consequences). It also permits the host to customize the CLR's response to a failed allocation, ranging from an OutOfMemoryException being thrown all the way up through the process being torn down. The host can also use this manager to recapture memory from the CLR by unloading unused app domains and forcing garbage collection. The memory manager interfaces are listed in Figure 1.

Figure 1 Memory Manager Interfaces

Interface Description
ICLRGCManager Provides methods that allow a host to interact with the CLR’s garbage collection system.
ICLRMemoryNotificationCallback Notifies the CLR of the memory load on the computer.
IHostGCManager Notifies the host that the thread from which the method call was made is about to block for a garbage collection.
IHostMAlloc Provides methods that allow the CLR to request fine-grained allocations from the heap through the host.
IHostMemoryManager Provides methods that allow the CLR to make virtual memory requests through the host instead of using the standard Windows platform virtual memory APIs.

SQL Server™ 2005 demonstrates the usefulness of the memory manager hosting APIs. SQL Server 2005 operates within a configurable amount of memory and is often set to use nearly all of the physical memory on the machine. To maximize performance, SQL Server 2005 tracks all memory allocations in an attempt to ensure that paging never occurs. The server would rather fail a memory allocation than page to disk. To accurately track all allocations, SQL Server uses the memory manager hosting APIs such that any memory allocation requests by the CLR are made through SQL Server rather than directly to Windows. This gives SQL Server the ability to fail CLR allocation requests before paging occurs.

Threading Manager

The new hosting APIs abstract the notion of a Win32 thread and essentially let the host define the unit of scheduling and execution. The hosting APIs use the term "Task" to define this abstraction. As part of abstracting Win32 threads, tasks provide a variety of methods for modifying the CLR.

The threading manager can:

  • Allow the host to provide an interface that the CLR will use to create and start new tasks.
  • Provide the host with a mechanism to "reuse" or pool the CLR-implemented portion of a task.
  • Support standard operations like start, abort, join, and alert.
  • Implement a callback to notify the CLR when a task has been moved to or from a runnable state. When a call is moved from a runnable state, the CLR must be able to specify that the task should be rescheduled as soon as possible.
  • Provide a way for the CLR to notify the host that a given task cannot be moved to a different physical OS thread and cannot have its execution blocked during a specified window.
  • Allow the host to provide an implementation of the thread pool. The CLR must be able to queue work items, set and query the size of the thread pool, and so on.
  • Provide notifications on both the host and CLR sides that the locale has been changed on a given task.
  • Provide a means for the CLR (and user code) to adjust the priority of a task.

Figure 2 shows the threading manager interfaces.

Figure 2 Threading Manager Interfaces

Interface Description
ICLRTask Provides methods that allow the host to make requests of the CLR or to provide notification to the runtime about the associated task.
ICLRTaskManager Provides methods that allow the host to request explicitly that the CLR create a new task, to get the currently executing task, and to set the geographic language and culture for the task.
IHostSecurityContext Allows the CLR to maintain security context information implemented by the host.
IHostSecurityManager Provides methods that allow access to and control over the security context of the currently executing thread.
IHostTask Provides methods that allow the CLR to start, awaken or abort tasks, and to associate an ICLRTask instance with a corresponding IHostTask instance.
IHostTaskManager Provides methods that allow the CLR to work with tasks through the host instead of using the standard operating system threading or fiber APIs.
IHostThreadPoolManager Provides methods that allow the CLR to configure the thread pool and to queue work items to it.

Synchronization Manager

While letting the host create tasks is sufficient for many multithreaded applications, some hosts also require the ability to override the CLR's synchronization primitives. This ensures that locks are not taken on an operating system thread without the host's knowledge, and it allows CLR tasks to further integrate with the host's scheduling mechanism as well as permitting the host to perform deadlock detection.

Using the synchronization manager allows the host to provide implementations for several synchronization primitives to the CLR, including critical sections, events (both manual and auto-reset), semaphores, monitors, and reader/writer locks. See the synchronization manager interfaces in Figure 3.

Figure 3 Synchronization Manager Interfaces

Interface Description
ICLRSyncManager Defines methods that allow the host to get information about requested tasks and to detect deadlocks in its synchronization implementation.
IHostCrst Serves as the host’s representation of a critical section for threading.
IHostManualEvent Provides the host’s implementation of a representation of a manual-reset event.
IHostAutoEvent Provides a representation of the host’s implementation of an auto-reset event.
IHostSemaphore Represents the host’s implementation of a semaphore for threading.
IHostSyncManager Provides methods that allow the CLR to create synchronization primitives by calling the host rather than through the standard Windows platform APIs.

I/O Completion Manager

The I/O completion manager lets the host provide a custom port implementation for the CLR to use in place of the default Windows I/O completion port functionality. The host provides a way for the CLR to bind a handle to an I/O completion port. In return, the CLR supplies a callback to be invoked by the host when an asynchronous I/O operation completes. In addition, this manager also allows the host to insert custom data at the end of the OVERLAPPED structure passed to the I/O routines. Figure 4 shows the I/O completion manager interfaces.

Figure 4 I/O Completion Manager Interfaces

Interface Description
ICLRIoCompletionManager Implements a callback method that allows the host to notify the CLR of the status of specified I/O requests.
IHostIoCompletionManager Provides methods that allow the CLR to interact with I/O completion ports provided by the host.

Assembly Load Manager

Hosts relying on the old hosting APIs customized assembly-loading behavior by catching events on System.AppDomain that are thrown when an assembly, resource, or type cannot be found (AssemblyResolveEvent, ResourceResolveEvent, and TypeResolveEvent). The requested assembly was then loaded by the host using the overload of Assembly.Load that accepts a byte array and is passed back from the event. This approach has proven insufficient for a few reasons, including performance. Loading an assembly by specifying a managed byte array involves copying the bytes multiple times between managed and unmanaged memory before the assembly can be run by the host.

In version 2.0 of the hosting APIs, the host specifies which assemblies are to be loaded from the Global Assembly Cache (GAC) and fulfills all other requests directly. When binding to an assembly, the host can return the assembly as a pointer to unmanaged memory (an unmanaged IStream *). In effect, this allows a host to implement a custom assembly store. In fact, this is the primary reason that SQL Server 2005 implements its own custom assembly load manager. The assemblies that make up a database application in SQL Server 2005 are physically stored in the SQL Server database rather than as separate files on disk. SQL Server 2005 uses the assembly load manager APIs to efficiently load application assemblies out of the database while continuing to rely on the usual .NET Framework mechanisms to load the assemblies. Figure 5 shows the assembly load manager interfaces.

Figure 5 Assembly Load Manager Interfaces

Interfaces Description
ICLRAssemblyIdentityManager Provides methods that support communication about assemblies between the host and the CLR.
ICLRAssemblyReferenceList Manages a list of assemblies that are loaded by the CLR and not by the host.
ICLRHostBindingPolicyManager Provides methods for the host to evaluate current binding policy and communicate policy changes for a specified assembly.
ICLRProbingAssemblyEnum Provides methods that allow the host to get the probing identities of an assembly using the identity information of that assembly that is internal to the CLR, without needing to create or understand that identity.
ICLRReferenceAssemblyEnum Provides methods that allow the host to manipulate the set of assemblies referenced by a file or stream using assembly identity data internal to the CLR, without needing to create or understand those identities.
ICLRValidator Provides methods for validation of portable executable images and detailed reporting of validation errors.
IHostAssemblyManager Provides methods that allow a host to specify sets of assemblies that should be loaded by the CLR or by the host.
IHostAssemblyStore Provides methods that allow a host to bind to assemblies and modules independently of the CLR.

CLR Configuration Manager

The CLR configuration manager gives access to interfaces (see Figure 6) that allow the host to configure several aspects of the CLR, including logically grouping tasks to simplify debugging, setting a custom heap dump configuration, registering callbacks for important CLR events, blocking certain functionality from being loaded into the CLR, and setting a critical failure escalation policy.

Figure 6 CLR Configuration Manager Interfaces

Interface Description
IActionOnCLREvent Provides the OnEvent method, which performs callbacks on events that have been registered using a call to ICLROnEventManager::RegisterActionOnEvent.
ICLRControl Provides methods that allow a host to get references to and configure aspects of the CLR.
ICLRDebugManager Provides methods that allow a host to associate a set of tasks with an identifier and friendly name.
ICLRErrorReportingManager Provides methods that allow the host to configure custom heap dumps for error reporting.
ICLRHostProtectionManager Provides the host with a way to block managed classes, methods, properties, and fields that offer specified capabilities from running in partially trusted code.
ICLROnEventManager Provides methods that allow the host to register and unregister callbacks for CLR runtime events.
ICLRPolicyManager Provides methods that allow the host to specify policy actions to be taken in the event of failures and timeouts.

The concept of an escalation policy deserves some explanation. Critical failures include resource allocation failures (such as out-of-memory conditions) and asynchronous failures (such as stack overflows), and every type of failure has a default behavior associated with it, ranging from throwing an exception through process termination. In version 2.0, the CLR enables a host to override the default behavior when a critical failure occurs. Advanced hosts can define a chain of events that will be tried in a sequence determined by the host to respond to failures. For example, a host may decide that every out-of-memory failure will translate into aborting the thread or unloading the AppDomain.

SQL Server 2005 uses escalation policy in part to ensure process stability by escalating thread aborts to AppDomain unloads when resource failures occur in areas of code that could affect multiple tasks. For example, suppose a task that's holding a lock receives a failure when trying to allocate memory. In this scenario, aborting just the current task is not sufficient to ensure stability of the AppDomain because there may be other tasks in the domain waiting on the same lock or expecting the data that was being manipulated to be in a consistent state. For more information on escalation policies and reliability, see Stephen Toub's article "High Availability: Keep Your Code Running with the Reliability Features of the .NET Framework" in the October 2005 issue of MSDN®Magazine.

Using the Hosting APIs

In his article "No More Hangs: Advanced Techniques to Avoid and Detect Deadlocks in .NET Apps" in the April 2006 issue of MSDN Magazine, Joe Duffy developed a thin host that intercepted thread and synchronization primitive operations to perform deadlock detection (the source code is available at msdn.microsoft.com/msdnmag/issues/06/04/Deadlocks). The code shows how to use the hosting APIs.

The code in Figure 7 is similar to what you'll need in every host you write. It loads the CLR into the process, starts it, and loads an assembly into the default AppDomain, invoking a method on it. (Error checking code is omitted for clarity.)

Figure 7 Typical Host Main Method

int main(int argc, _TCHAR* argv[])
{
    // Bind to the runtime.
    ICLRRuntimeHost *pClrHost = NULL;
    HRESULT hrCorBind = CorBindToRuntimeEx(
        NULL,   // Load the latest CLR version available
        L"wks", // Workstation GC ("wks" or "svr" overrides)
        0,      // No flags needed
        CLSID_CLRRuntimeHost,
        IID_ICLRRuntimeHost,
        (PVOID*)&pClrHost);
 
    // Construct our host control object.
    DHHostControl *pHostControl = new DHHostControl(pClrHost);
    
    // Notify the CLR that this host implements hosting managers.
    pClrHost->SetHostControl(pHostControl);

    // Now, start the CLR.
    HRESULT hrStart = pClrHost->Start();

    // Load an assembly and execute a method in it.
    HRESULT hrExecute = pClrHost->ExecuteInDefaultAppDomain(
        pwzAssemblyPath, pwzAssemblyName,
        pwzMethodName, pwzMethodArgs,
        &retVal);
}

The first task in hosting the CLR is getting a pointer to the ICLRRuntimeHost interface from the CLR:

ICLRRuntimeHost *pClrHost = NULL;
HRESULT hrCorBind = CorBindToRuntimeEx(
    NULL, // Load the latest CLR version available
    L"wks", // Workstation GC ("wks" or "svr" overrides)
    0, // No flags needed
    CLSID_CLRCLRHost,
    IID_ICLRRuntimeHost,
    (PVOID*)&pClrHost);

In the .NET Framework 1.x you'd use ICorCLRHost to communicate between the CLR and the host; in version 2.0, ICLRRuntimeHost is the recommended interface, as it allows you to access all the new opt-in functionality described here. Among other functions, ICLRRuntimeHost facilitates the exchange of hosting interface implementations between the CLR and the host. ICLRRuntimeHost::SetCLRControl allows the host to notify the CLR that some host-defined services need to be hooked up to override the CLR's default behavior. Our example uses the DHHostControl class, which implements IHostControl, to override some of the CLR's default managers:

DHHostControl *pHostControl = new DHHostControl(pClrHost);
pClrHost->SetHostControl(pHostControl);

Once the host successfully calls SetHostControl, the CLR will use any interfaces defined in the host's IHostControl implementation in place of the default actions. In this example, DHHostControl overrides IHostTaskManager and IHostSyncManager by providing two classes in DHHostControl that inherit from these interfaces.

ICLRRuntimeHost also allows the host to get pointers to the CLR's implementation of the ICLR* interfaces. This is done by calling the ICLRRuntimeHost::GetCLRControl method to get a pointer to an ICLRControl object. ICLRControl lets the host access all other ICLR* interfaces through which the host can request specific actions from the CLR. Since the sample application is monitoring the CLR, it does not use the ICLR* interfaces.

Once you've loaded and started the CLR, the easiest way to load and run your managed code is to use the ExecuteInDefaultApp-Domain method on ICLRRuntimeHost. This will simply load a managed assembly and execute a method on it:

HRESULT hrExecute = pClrHost->ExecuteInDefaultAppDomain(
    pwzAssemblyPath, pwzAssemblyName, 
    pwzMethodName, pwzMethodArgs,
    &retVal);

Conclusion

Unmanaged hosts are able to control very fine-grained details of the internal workings of the CLR. Using the new .NET Framework 2.0 hosting APIs, a host can, in fact, place itself between the CLR and the operating system and broker any request from the CLR. You can find more information in the online documentation of the hosting APIs and in the book Customizing the Microsoft .NET Framework Common Language Runtime by Steven Pratschner (Microsoft Press®, 2005).

Send your questions and comments to  clrinout@microsoft.com.

Alessandro Catorcini is a lead program manager in the CLR team. In Visual Studio 2005 he was responsible for the hosting API layer and CLR integration into SQL Server 2005.

Piotr Puszkiewiczis a program manager in the CLR team. He is currently responsible for the hosting API, the CLR exception system, and Managed Debugging Assistants.