CLR Hosting

Introduction

The last couple of days I've been playing around with some stuff around WCF (formerly known as Indigo for the people who've been living on Mars lately). Instead of hosting stuff in IIS I decided (for no specific reason whatsoever) to take up the hosting stuff foreseen by WCF and to host my app inside a Windows Service. Pretty easy to do. However, there was one little problem that bothered me as I wanted to have some way to update the app without stopping the Windows Service that's responsible to host the app's functionality. In reality, what should happen is that the new version of the app core has to be loaded side-by-side with the old version till all clients connected with that old version are disconnected. Exactly what ASP.NET allows you to do in its bin folder. Enter the world of shadow copying.

A little example

Let's come to the point. How to enable this kind of stuff in your own application? A really simple and short example:

First, write a method to create a new app domain to host the new version of the assembly. As you might know, it's impossible to unload an assembly once loaded in an appdomain, you can only unload the associated appdomain entirely. What the following method does is pretty straightforward: it tells the CLR (assembly loader) where to look for files and then tells it to enable shadow copying by setting the (wrong-typed; a boolean property would have been much better) ShadowCopyFiles property to the string "true". Next, an appdomain is created with a unique name using some counter, and the assembly with the functionality is loaded (notice the _ indicates private members of the current class). This method is called when the app starts.

private void LoadAppDomain()
{
     AppDomainSetup setup = new AppDomainSetup
();
     setup.ApplicationBase =
"c:\\temp"
;
     setup.ShadowCopyFiles =
"true"
;

     AppDomain domain = AppDomain.CreateDomain("ShadowCopy domain " + _domainNumber, null
, setup);
     _currentAssembly = domain.Load(
"Server", null
);

     _domainNumber++;
}

The second piece of code required is the use of a FileSystemWatcher to detect when the original file is changed. This is done by watching (in this case) Server.dll in the "c:\temp" folder. When a change is detected, LoadAppDomain gets called. It's a little unfortunate the FileSystemWatcher triggers multiple events whenever a file is changed, so you need some plumbing to capture the event only once. This can be done using some datetime tracking or you can just keep a boolean field indicating the file was changed and load it upon arrival of a client.

Another thing to think about is how to unload the (old) appdomain(s). Doing it is as simple as calling AppDomain.Unload passing in the appdomain to be unloaded as the first parameter. The question however is when to do the unload. This will depend on the type of clients you are serving and mechanisms you have to detect no one is active in the appdomain anymore (which will be dependent on, say, the stateless or stateful nature of the app, just to name one thing). For the moment, in my app, there's just a timer that unloads "idle" appdomains after a couple of minutes from loading the new appdomain.

The last thing you have to do is cope with the clients in some way and send them to a "service" class that's defined in the Server.dll file. How you want to do this, depends on your (activation) scenario (such as singleton, singecall, ... - notice these indicate concepts, not .NET Remoting-specific technology pieces). A possiblity to create instances of the underlying service class is this:

IServer srv = (IServer)_currentAssembly.CreateInstance("ServerNamespace.Server");

Conclusion

Without going to much in depth on the specific scenario I was working on (maybe I'll write about this later on, if time and Belgian summer temperatures permit) you saw how easy it is to turn on shadow copying when creating an AppDomain. This feature keeps the assembly free for replacement while the app is running (read: no file locks). You might want to check out other AppDomainSetup members too, like ShadowCopyDirectories to limit the folders that are under shadow copy "protection".

Take a look at Suzanne Cook's blog too over here.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Introduction

Part 4 of my CLR Hosting journey: scheduling and threading. Today's applications typically rely on threading primitives to perform multiple tasks concurrently. Schedulers are needed in the operating system (and on other places, as I'll show later) to plan the execution of these tasks, because just one task can run at a time on one processor. The CLR, being a layer on top of the Windows OS, has support for threading too (luckily). In the previous versions of the CLR, threading was handled by the operating system directly because the mscorwks.dll (or the server builds) did send calls for threading stuff to the Win32 API directly. Just as it was the case with memory management, the CLR in v2.0 of the .NET Framework has an intermediate layer to forward threading-calls to the host for further processing. The Default CLR Host will call into the Win32 API to perform all of the threading/scheduling, whileas other hosts can choose to take responsibility over this. Again, SQL Server 2005 was the key influence for the CLR team to adapt the design of it to allow customization by hosting applications. The stuff that's covered includes:

  • Task creation and management
  • Thread pool support
  • Synchronization mechanism support (e.g. mutex, semaphore, etc)

In this post, I'll start with the needs of SQL Server 2005 concerning the CLR's scheduling/threading mechanisms. Once I've explained this, we'll jump into mscoree again to explain how a CLR Host can take advantage of the various APIs to control scheduling and threading stuff in pretty much detail.

 

Threading and scheduling in SQL Server

The basics

It's clear that a database server such as SQL Server should be capable of doing a lot of work at (virtually) the same time. In a multi-user database environment, it's key to achieve a great performance and scalability in order to be competent with other vendors. This brings us to the need for an efficient scheduling mechanism to allow the database engine to do various things at a time, in order to server users a fast as possible to process their requests. SQL Server uses a mutli-threading model to accomplish this goal, while running in one single process. This means there is one process with multiple thread and one single memory address space, therefore eliminating all stuff around shared memory. Now, okay, we do have multiple threads, but how are those threads put on the processor to perform work? That's where the User Mode Scheduler comes into play. The User Mode Scheduler (further abbreviated to UMS) is a key component of SQL Server that gives the SQL OS (that's the core engine of SQL Server in relation to resource allocation and control) more control over how threads (and fibers, see further) are scheduled. Every processor has one instance of the UMS associated with it (well, that's only true for the processors that SQL Server is allowed to use, which is configurable trough the affinity mask).

So, we know there are threads being used by SQL Server. Next, you should know that these threads are grouped into thread pools which are dedicated for various kinds of operations. First of all, there is a series of threads that are out there to support the basic core tasks of the SQL Server database engine, such as the lazywriter, the log writer (which writes out log caches - in-memory representations of transaction log entries - to disk based on a flushQueue and a freeQueue to return written-to-disk-caches to), cleanup threads, etc. Beside its own housekeeping, SQL Server should also be able to process requests from users and client applications to retrieve/manipulate data (that's what a database is for, right?). In order to do this, there's a pool of so-called worker threads. Clients submit requests to the SQL Server database through the Net-Library up to the User Mode Scheduler before it reaches the Relational Engine. This is where the UMS kicks in to assign a worker thread to the incoming request for further processing. This request submission is done through an IO completion port through which it's queued up in a completion queue. There are various actions the scheduler can take to assign a thread: either it can take an available thread from the pool, or it can create a new thread (there is a configurable upper limit called max worker threads that defaults to 255 which should be okay), put it in the pool and assign it to the client's request for further processing. The assigned worker thread stays alive and bound to the user's request till completion of the request. Worker threads are divided among the different UMS schedulers that are running on the machine (remember, one for each processor).

Threads versus fibers

SQL Server support two modi which it can run in. The first one is the default and is called "thread mode", the other one is called "fiber mode". In order to understand the difference, let's explain the difference between threads and fibers:

  • Threads are kernel-mode objects known by the OS and act as the default unit of work which can be put on the scheduler. At regular times the (preemptive) scheduler of the Windows OS starts its mission to take a thread off the processor (or off on of the processor on an SMP machine), find another thread (by scanning the list of threads and executing some thread election algorithm, based on thread priorities, credits, etc) and put that thread on the processor to start executing. Switching between threads is called on context-switch and has a relatively high overhead on the system.
  • Fibers are often called lightweight threads and are living for 100% in user mode, so the Windows OS's scheduler doesn't even know about their existence. Code running in user mode is responsible for all of the scheduling work for the fibers to run.

Fiber mode (or lightweight pooling) looks promising but there are a couple of drawbacks, one of those being the fact that not all of the functionality (e.g. SQLMail - if you should use that - and SQLXML) does work in fiber mode. The key recommendation however is to avoid fibers if you can. Tempting as the checkbox in the SQL Server configuration might be, it's generally spoken not a good idea to enable fiber mode (see MSDN too on http://msdn.microsoft.com/library/en-us/dnsqldev/html/sqldev_02152005.asp?frame=true). One scenario where fibers might be advisible is when the overhead of context switching is so high that it starts to hurt other work that needs to be done by the server.

The User Mode Scheduler

In this paragraph, I'll explain the basics of the User Mode Scheduler. If you want to know more about it, I recommend you to take a look at the book "The Guru's Guide to SQL Server Architecture and Internals" by Ken Henderson, chapter 10. First of all there's the UMS thread running on every processor that can be used by SQL Server. There's only one such thread per processor. It's the task of the OS to schedule this thread on any of the available processors. When you do configure an affinity mask you tell SQL Server which CPUs it can use which is likely not a good idea because it causes overhead on the UMSs to schedule their threads on the same processor every time (the OS simply is prevented from choosing any free processor to put the UMS' thread on).

All code running in SQL Server (because of user requests for example, performing transactions, queries, etc) is scheduled by the UMS. If there's non-SQL Server code that has to be run, the UMS will keep its hands off it and leave it to the OS's scheduler to perform the work (e.g. extended procedures). The reason to introduce a UMS to control the scheduling originates from the SQL Server 7.0 timeframe where it became clear that relying on the underlying OS to perform all of the scheduling was not flexible enough and the scalability potential of the product was hindered because of this.

The UMS lives in a file called ums.dll inside the Binn folder of your SQL Server installation. The overall goal of the UMS is to avoid wasting processor cycles because of things such as context switches and a thread switching between user and kernel mode. The UMS contains a subset of the functionality of the Win32 threading functionality, containing only those things that are relevant for SQL Server. When the UMS performs its work, it calls into the Win32 functions to do further work, after controlling SQL Server's needs to achieve high scalability and performance. Actually the UMS is nothing more than a thin layer between the OS and the database engine when it comes down to scheduling and threading.

Now, the big difference between SQL Server's scheduling and the scheduling implemented by the Windows OS. SQL Server's UMS performs so-called cooperative scheduling whileas the Windows OS (since NT) works based on preemptive scheduling. What do these two terms mean?

  • Preemptive scheduling: The scheduler doesn't trust anyone and kicks in at regular times to take a currently running thread from the processor it was assigned to, to save that thread's state (stack, program counter, registers, etc), find another thread to run and put that thread on the processor by restoring its state and waking it up. This way, no single program or thread can monopolize the processor which can bring down the computer (in a sense that no-one else is allowed to run, e.g. explorer.exe would starve, the OS seems to hang).
  • Cooperative scheduling: "We're friends, aren't we?" The UMS knows it can trust the threads that are part of the game and that these threads will voluntarily yield. As the matter in fact, all of the thread's code inside SQL Server is implemented by the SQL Server team, so it's possible to know the behavior of it. However, if there is one single thread that does not yield (and thus acts as a bad guy), the whole database system will be hurt because of this.

The UMS uses the Win32 API to reach its goal on an underlying preemptive OS. The basic mechanism it employs is the following. Every thread running in a UMS has an event object that can be signaled. When the corresponding UMS for the thread does not want the thread to be scheduled (cf. worker thread pool etc), it asks that thread to start an infinite wait on its event object by calling WaitForSingleObject. Because of this, the OS will judge that this thread is waiting and can't possibly do anything, therefore bypassing it when looking (in a preemptive manner) for a thread to be put on the processor to start its work. At a certain point in time, the UMS might want the thread to start doing something and signals the event object of that thread. Because of this, the WaitForSingleObject call ends and the thread comes alive. Now, the Windows scheduler will see that the thread is alive and kicking and ready to do some work, so it can be selected by the scheduler to run on some processor. In order to avoid context switches and swapping on processors, the UMS will attempt to keep the number of viable threads as low as possible, ideally having only one thread per processor. That way, the Windows OS has no choice but to select that "dedicated to a given processor" thread.

To run extended procedures, the UMS can't even think of controlling it in a cooperative scheduled fashion because the code in the extended procedure can't be trusted and it can't even indicate to yield. Because of this, preemptive scheduling for extended procedures (and other sort of stuff such as OAs, debugging, distributed queries, etc) is used. On its turn, this causes a drop on scalability and concurrency because the UMS isn't able anymore to serve a lot of requests on a limited set of workers.

An explanation of the UMS scheduler itself and all of the associated lists would bring us too far. Instead, I'll just mention these and give a short one-line description:

  • Worker list - contains all of the available UMS workers, encapsulating a fiber or thread
  • Runnable list - all of the UMS workers ready to execute but waiting to be signaled by the UMS (which is done indirectly by another worker calling yield)
  • Waiter list - UMS workers waiting for a resource; another worker that holds the resource is responsible to signal the waiting worker by scanning the waiter list
  • I/O list - list containing outstanding asynchronous I/O requests waiting for completion; any yielding worker has to walk through this list to check for completed asynchronous I/O operations and to wake the corresponding worker
  • Timer list - list with timed work requests; any yielding worker has to walk through this list to check for expired timer requests and to wake the corresponding worker

As you can see, voluntarily thread yielding is a key job for each of the workers.

You can check the statistics of the UMS by calling DBCC SQLPERF(umsstats):

Statistic                        Value                   
-------------------------------- ------------------------
Scheduler ID                     0.0
  num users                      16.0
  num runnable                   0.0
  num workers                    12.0
  idle workers                   5.0
  work queued                    0.0
  cntxt switches                 15491.0
  cntxt switches(idle)           18763.0
Scheduler Switches               0.0
Total Work                       11951.0

(10 row(s) affected)

DBCC execution completed. If DBCC printed error messages, contact your system administrator.

Note, the results shown above are these from a system that has just been started a couple of minutes ago. On a system that's up and running for a longer time, you should see far higher numbers for the total work and the context switches.

 

CLR Hosting

In this first post about scheduling and threading in the CLR Hosting APIs I'll give a brief overview of the basic concepts and API functions you should know about. In the next posts of CLR Hosting part 4, I'll dig into more details by taking a look with you at the CoopFiber sample that's included in the .NET Framework v2.0 SDK. More information about this sample can also be found on Robert "Dino" Viehland's blog. Dino's working on the CLR team as an SDE/T and has been posting about this sample too in the past.

Tasks

Okay, so now how does SQL Server 2005 can make sure that cooperative scheduling can be combined with the CLR integration stuff. Before v2.0 of the CLR, the CLR was only capable of working on a preemptively scheduled operating system layer. The hosting APIs did not provide a means to put the CLR on top of a cooperatively scheduled "host". As a basic notion, the CLR Hosting API provides the notion of a task which is the abstraction of an underlying unit of execution, being either a thread or a fiber (see above for the explanation of both terms). Based on the mode SQL Server 2005 is running in, the notion of a task is mapped either to a thread or a fiber on the OS.

Basically, there are 4 interfaces:

  • IHostTaskManager - interface to create your own task manager that the CLR should obtain a reference of through the IHostControl::GetHostManager method as explained in previous posts
    • This interface provides support to create tasks, perform switching between tasks, putting a task asleep, indicate managed/unmanaged code transitions, etc
  • ICLRTaskManager - the IHostTaskManager interface contains a method called SetCLRTaskManager, which is used by the CLR to pass in an instance of an ICLRTaskManager object; this is the side of the CLR in relation to task management
    • In here you'll find a limited set of functionality that's similar to the functionality in the IHostTaskManager. I'll cover this further in this post.
  • IHostTask - the host's notion of a task
    • Used to start, stop, join, alert tasks and to get/set priorites.
  • ICLRTask - the CLR's notion of a task; as with the ICLRTaskManager, the CLR provides the IHostTask instance with an instance of ICLRTask through a method called SetCLRTask
    • This interface contains methods to communicate to the CLR, for example to notify it that the host is scheduling/unscheduling a task (SwitchIn/SwitchOut methods). Other functionality includes yielding (cf. SQL Server elaboration on cooperative scheduling), an ExiTask function and functions to Abort and RudeAbort (the diffence will be explained later) a task. There are also functions for statistics information (memory statistics, number of held locks).

A task has a lifecycle that you should understand in order to implement the IHost* interfaces correctly. Let's show such a lifecycle:

  1. A task is created and starts its live in an unscheduled state. The CLR calls the IHostTaskManager::CreateTask method and creates an instance of ICLRTask which is passed to the IHostTask through the SetCLRTask method.
  2. The task is started by calling IHostTask::Start.
  3. The host decides to schedule the task. Using the ICLRTask reference it got in step 1, it calls SwitchIn to tell the CLR that the task will be scheduled.
  4. Now the task is up and running.
  5. When the host decides to switch out the task, it can call the SwitchOut method on the ICLRTask object, in a similar way as explained in step 3. The host can decide this based on various conditions, including I/O completion (e.g. relation to the I/O List in the UMS of SQL Server) or synchronization blocking (e.g. access to a resource protected with mutex/semaphore).
  6. Later on, the task can be scheduled again, using SwitchIn as explained in step 3. However, this time it might end up on another physical OS thread (when being mapped to threads). The CLR can indicate thread affinity however when it needs this for the particular thread. It does this by calling BeginThreadAffinity and EndThreadAffinity on the IHostTaskManager. In that case, the host must reschedule the task on the same OS thread.
  7. A last situation occurs when the task has completed its work. In that case, the host calls ICLRTask::ExitTask to notify the CLR about this.

Aborts and rude aborts will be covered later. Notice however that these are communicated to the CLR too by the host. This stuff has to do with finalization as I'll explain later on in further posts. Finally, there is a Reset method on ICLRTask too, to clean up the task and to enabe it to be reused by the CLR in the future (rather than calling ExitTask to destroy the task on the CLR level).

Note: Beside of threads created through the BCL functions (and later on, redirected to the host to perform the actual work) such as the Thread class, the CLR creates some management threads when it's loaded through the Win32 API always. These threads are responsible for various tasks such as garbage collection (gc and finalizer), debugging (debugger helper thread), thread pool access control (timer, gate, worker, waiter and I/O completion thread), internal timing.

Managed/unmanaged code execution transitions

Transitions between unmanaged code and managed code (in both directions) can be intercepted by providing a host manager for scheduling too. Examples include the use of COM interop to call unmanaged code from within a managed code context and the use of function pointers (in managed code that corresponds to a delegate) to call back into managed code by using such a pointer that was marshaled to native code. Hosts that use cooperative scheduling (such as the SQL Server 2005 CLR Host) need to know when execution leaves managed code, because from that point on, threading is no longer under the control of the CLR. Therefore, the host can't control the scheduling anymore (refer to the voluntary yielding of threads where the mechanism of cooperative scheduling is built on), so it has to grant the thread to be scheduled preemptively by the OS to continue its work.

In order to support those scenarios, the CLR calls the host ot notify the host about these managed/unmanaged code transitions using four methods on the IHostTaskManager object:

  • LeaveRuntime - we're going to unmanaged code now (preemptive scheduling required)
  • EnterRuntime - we're back in managed code, under the control of the CLR (cooperative scheduling possible)
  • ReverseEnterRuntime - unmanaged native code calls back to managed code through a function pointer
  • ReverseLeaveRuntime - the managed code execution initiated by unmanaged code (see previous bullet) stops executing, causing a transition to unmanaged code again

Leave* and Enter* methods are always paired.

 

To be continued

In the next subpart of CLR Hosting part 4, I'll cover the stuff around synchronization management and thread pooling. After having covered this stuff, I'll take a look at the CoopFiber sample together with you.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

I just launched my VPC with the beta 2 bits of the .NET Framework v2.0 (it's currently not on my primary OS installation as that one is currently under high other-beta-pressure, but things will change soon when I receive a new harddisk which I'll partition in a few OS install partitions, including one for Vista beta 1) and did check the mscoree.idl file for the changes to the IHostMemoryManager I told you about in the previous part 3 post of my CLR Hosting series. Here are the renewed interfaces:

[
    uuid(7BC698D1-F9E3-4460-9CDE-D04248E9FA24),
    version(1.0),
    helpstring("Host memory manager"),
    pointer_default(unique),
    local
]
interface IHostMemoryManager_DeleteMe : IUnknown //[BDS] Note: this is the interface I've shown you previously
{
    HRESULT CreateMalloc([in] BOOL fThreadSafe,
                         [out] IHostMalloc **ppMalloc);

    HRESULT VirtualAlloc([in] void*       pAddress,
                         [in] SIZE_T      dwSize,
                         [in] DWORD       flAllocationType,
                         [in] DWORD       flProtect,
                         [in] EMemoryCriticalLevel eCriticalLevel,
                         [out] void**     ppMem);

    HRESULT VirtualFree([in] LPVOID      lpAddress,
                        [in] SIZE_T      dwSize,
                        [in] DWORD       dwFreeType);

    HRESULT VirtualQuery([in] void *     lpAddress,
                         [out] void*     lpBuffer,
                         [in] SIZE_T     dwLength,
                         [out] SIZE_T *  pResult);

    HRESULT VirtualProtect([in] void *       lpAddress,
                           [in] SIZE_T       dwSize,
                           [in] DWORD        flNewProtect,
                           [out] DWORD *     pflOldProtect);

    HRESULT GetMemoryLoad([out] DWORD* pMemoryLoad,
                          [out] SIZE_T *pAvailableBytes);

    HRESULT RegisterMemoryNotificationCallback([in] ICLRMemoryNotificationCallback * pCallback);
}

typedef enum
{
    MALLOC_THREADSAFE = 0x1,
    MALLOC_EXECUTABLE = 0x2,
} MALLOC_TYPE;

[
    uuid(7BC698D1-F9E3-4460-9CDE-D04248E9FA25),
    version(1.0),
    helpstring("Host memory manager"),
    pointer_default(unique),
    local
]
interface IHostMemoryManager : IUnknown //[BDS] Note: this is the renewed interface
{
    HRESULT CreateMalloc([in] DWORD dwMallocType, //[BDS] Note: change I mentioned previously too
                         [out] IHostMalloc **ppMalloc);

    HRESULT VirtualAlloc([in] void*       pAddress,
                         [in] SIZE_T      dwSize,
                         [in] DWORD       flAllocationType,
                         [in] DWORD       flProtect,
                         [in] EMemoryCriticalLevel eCriticalLevel,
                         [out] void**     ppMem);

    HRESULT VirtualFree([in] LPVOID      lpAddress,
                        [in] SIZE_T      dwSize,
                        [in] DWORD       dwFreeType);

    HRESULT VirtualQuery([in] void *     lpAddress,
                         [out] void*     lpBuffer,
                         [in] SIZE_T     dwLength,
                         [out] SIZE_T *  pResult);

    HRESULT VirtualProtect([in] void *       lpAddress,
                           [in] SIZE_T       dwSize,
                           [in] DWORD        flNewProtect,
                           [out] DWORD *     pflOldProtect);

    HRESULT GetMemoryLoad([out] DWORD* pMemoryLoad,
                          [out] SIZE_T *pAvailableBytes);

    HRESULT RegisterMemoryNotificationCallback([in] ICLRMemoryNotificationCallback * pCallback);

    HRESULT NeedsVirtualAddressSpace(
        [in] LPVOID startAddress,
        [in] SIZE_T size
        );

    HRESULT AcquiredVirtualAddressSpace(
        [in] LPVOID startAddress,
        [in] SIZE_T size
        );

    HRESULT ReleasedVirtualAddressSpace(
        [in] LPVOID startAddress
        );

}

As you can expect, the first one (suffixed with _DeleteMe) will be removed later on, as mentioned somewhere else in the IDL too:

//!!! Delete this one only after SQL integrates Whidbey beta2 bits.
// IID IHostMemoryManager_DeleteMe : uuid(7BC698D1-F9E3-4460-9CDE-D04248E9FA24)
cpp_quote("EXTERN_GUID(IID_IHostMemoryManager_DeleteMe, 0x7BC698D1, 0xF9E3, 0x4460, 0x9C, 0xDE, 0xD0, 0x42, 0x48, 0xE9, 0xFA, 0x24);")

Developer's notes everywhere, that's why I like beta software :-). You can even find places with DeleteMe2 suffixes for the moment :o. It also shows the versioning hell of pre-.NET stuff.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Introduction

A first specific aspect of CLR Hosting I want to cover in this blog post series is the memory management part of the CLR Hosting APIs. It all started some years ago when the SQL Server team started to look at the possibilities to integrate the CLR into the SQL Server database engine to give developers the opportunity to use their knowledge of managed code development to extend the functionality of the database, by writing things such as stored procedures, triggers, user defined functions, user defined types in their favorite languages. One of the reasons of considering this integration was undoubtly the gain of developer productivity and the possibility to integrate database development tighter with the Visual Studio IDE tools, but also the characteristics of the CLR and the .NET Framework as such have played an important role in this decision. For example, extending the functionality of the database using an eXtended Procedure in C++ is a very dangerous thing, whileas doing the same in a managed environment using the CLR eliminates a lot of risks that are introduced by running custom code in the core of the database engine. Things such as memory management, type safety, exception handling mechanisms, etc are great aspects of the CLR and were considered to be a welcome gift in the SQL Server developers world.

However, integrating the CLR in a product like SQL Server which has very high needs on the field of performance, scalability, reliability and data integrity (transactions you know) is not as easy as it might look in the first place. One example of the difficulties that kick in is the management of memory allocation and all of the stuff around it. Why is this? Well, as I said SQL Server has a high need for performance and wants to do everything it can to keep this as high as possible and to avoid conditions that can undermine the runtime quality attributes of the product. The way to do this is in SQL Server is called the SQL Server OS, which can be seen as being the heart of the product. As the name suggests, this core part of the database engine acts like a mini OS because it's responsible to control all threading, locking and scheduling stuff (User Mode Scheduler, fibers, cooperative scheduling) but also all of the stuff around memory management. The latter one is the thing I'll be focusing on in this post.

 

Case study: How SQL Server manages memory

In order to ensure the performance and scalability of a SQL Server instance, the database engine needs a way to control and track all of the memory allocations that are done. It does so to ensure that the amount of allocated memory is kept between boundaries (by default, SQL Server will get as much memory as it can) and to avoid pages to be swapped to disk because the speed gap between primary (RAM) and secundary memory (disk) can lead to a serious performance drawback. Generally spoken, Windows offers applications three ways of allocating memory: virtual memory, shared memory and heaps. The first one is the primary means by which SQL Server allocates memory when that's needed. The virtual memory functionality in the Windows OS can be summarized by explaining the some key functions in the Win32 API to work with this mechanism:

  • VirtualAlloc - allocates memory in the address space of the process that requests the memory (there is an extension to this - suffixed with Ex - that allows more complex memory allocation)
  • VirtualFree - used to free the memory that was allocated previously using VirtualAlloc
  • VirtualLock and VirtualUnlock are used to lock virtual memory pages in the physical memory
  • VirtualProtect is used to change the protection attributes of a virtual memory page; examples of these attributes are PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READ_WRITE, etc including support for copy-on-write mechanisms and so on
  • VirtualQuery - obtain information about the virtual memory on the system

So, when we do want to embed the CLR inside SQL Server, we need to make sure SQL Server still can track all of the memory that's allocated to provide database functionality (e.g. in a managed stored procedure). The CLR Hosting API allows us to do this, by providing a memory management provider that the CLR will call every time it needs memory to do its job. Instead of sending these calls to the Win32 functions directly, these are sent to the SQL Server OS (more specifically the Memory Management Subsystem in there) to handle the request for memory, given the constraints of the SQL Server configuration concerning memory (e.g. min/max mem settings).

I won't cover the heap and shared memory in much detail over here. Instead I'll just mention the most important aspects of these. Let's start with the heap. A heap is controlled by a thing called a heap manager and consists of a region in memory divided in pages of reserved space. The heap manager can be called to get a piece of memory to work with. Typically heaps are used when objects or structures with similar sized need to be allocated in memory. An example of the usage of a heap is the new operator in C++ or the malloc function in C. The basic functions for heap management are:

  • HeapCreate and HeapDestroy are used to create and destroy so-called private heaps.
  • HeapAlloc allocates memory from the heap (cf. C's malloc)
  • HeapFree releases memory from the heap (cf. C's free)

Shared memory is the last mechanism that Windows offers to work with memory. The idea of shared memory is to allocate a memory region and to allow shared access to it for multiple processes, so it can be used for inter process data exchange. SQL Server uses shared memory as a fast way to communicate with client applications on the same machine, bypassing protocols such as TCP/IP (and therefore the whole network OSI stack) or named pipes, through the Net-Library related functionality. The basic functions include:

  • CreateFileMapping to create a so-called section object to be used with either shared memory or a memory-mapped file
  • MapViewOfFile creates a mapped view for a file in the physical memory
  • FlushViewOfFile writes modified pages in a mapped view to disk

Details about all of these functions can be found in the Platform SDK of Windows XP and Windows Server 2003.

 

Controlling Virtual Memory

In the section above, I presented a list of functions in the Win32 API that are used to manage virtual memory, including VirtualAlloc, VirtualFree, VirtualQuery and VirtualProtect. The CLR Hosting API provides an interface called IHostMemoryManager that exposes similar functionality to perform this kind of work. So, when the CLR needs to do something related to virtual memory, it will check whether a custom memory manager was hooked in by the host. If that's the case, the CLR will call that manager instead of calling the Win32 API directly to obtain, free, ... virtual memory.

The full interface of IHostMemoryManager is shown below:

interface IHostMemoryManager : IUnknown
{
    HRESULT CreateMalloc([in] BOOL fThreadSafe,
                         [out] IHostMalloc **ppMalloc);   
   
    HRESULT VirtualAlloc([in] void*       pAddress,
                         [in] SIZE_T      dwSize,
                         [in] DWORD       flAllocationType,
                         [in] DWORD       flProtect,
                         [in] EMemoryCriticalLevel eCriticalLevel,
                         [out] void**     ppMem);
   
    HRESULT VirtualFree([in] LPVOID      lpAddress,
                        [in] SIZE_T      dwSize,
                        [in] DWORD       dwFreeType);
   
    HRESULT VirtualQuery([in] void *     lpAddress,
                         [out] void*     lpBuffer,
                         [in] SIZE_T     dwLength,
                         [out] SIZE_T *  pResult);
   
    HRESULT VirtualProtect([in] void *       lpAddress,
                           [in] SIZE_T       dwSize,
                           [in] DWORD        flNewProtect,
                           [out] DWORD *     pflOldProtect);
   
    HRESULT GetMemoryLoad([out] DWORD* pMemoryLoad,
                          [out] SIZE_T *pAvailableBytes);
   
    HRESULT RegisterMemoryNotificationCallback([in] ICLRMemoryNotificationCallback * pCallback);
}

The most interesting functions in here are the ones that start with Virtual, as these are the equivalent of the well-known Win32 API functions. Actually, when you look at the Platform SDK, you'll find all of the stuff you need to know to understand the corresponding functions in the CLR Hosting API:

The explanation of the CLR Hosting virtual memory management functions can be found over here. One significant difference between the Win32 and CLR functions is the VirtualAlloc function's parameters. VirtualAlloc in the CLR Hosting API has an additional input parameter of the enumeration type EMemoryCriticalLevel:

typedef enum
{
    eTaskCritical = 0,
    eAppDomainCritical = 1,
    eProcessCritical = 2
} EMemoryCriticalLevel;

The CLR will use this parameter to inform the host about the consequences when the memory request is denied. E.g. if the eProcessCritical value is supplied, the result of denying the memory allocation can be a process termination. The greater than operator follows the gradation of severity when memory allocation fails (process > app domain > task).

The VirtualAlloc function can return several values, including S_OK to indicate the memory allocation succeeded, E_FAIL to report a catastrophic event (causing the CLR in the process to become unavailable) and E_OUTOFMEMORY (the host has decided the CLR can't get any memory right now). In the Win32 API the return value is actual the pointer to the allocated memory instead of an HRESULT value. In a similar fashion, the other Virtual* functions provide logical wrappers around the equivalent Win32 functions.

 

Heap management

The IHostMemoryManager isn't responsible for heap management by itself. Instead, it provides a function CreateMalloc to obtain an instance to a IHostMalloc object that controls heap memory:

    HRESULT CreateMalloc([in] BOOL fThreadSafe,
                         [out] IHostMalloc **ppMalloc);

The first parameter in here indicated whether thread safety is needed. The .NET Framework 2.0 build I'm using is actually a little outdated as newer releases (> 2.0.40607) have replaced this with a MALLOC_TYPE enumeration only consisting of two values. I won't cover this in detail now. The IHostMalloc interface looks as follows, offering three functions:

interface IHostMalloc : IUnknown
{
    HRESULT Alloc([in] SIZE_T  cbSize,
                  [in] EMemoryCriticalLevel eCriticalLevel,
                  [out] void** ppMem);
   
    HRESULT DebugAlloc([in] SIZE_T      cbSize,
                      [in] EMemoryCriticalLevel       eCriticalLevel,
                      [in] char*       pszFileName,
                      [in] int         iLineNo,
                      [out] void**     ppMem);

    HRESULT Free([in] void* pMem);
}

Alloc should be self-explanatory I guess, given the explanation of the EMemoryCriticalLevel type. DebugAlloc is used in debugging scenarios and allows to link to a source code file and a line number in that file. The Free function should ring a bell too for everyone who has ever done malloc/free stuff in plain old C.

 

File mapping

In the interface listing of IHostMemoryManager above, this stuff is not present yet (again because of the use of an early build). However, the second release of the .NET Framework CLR Hosting APIs will allow you as a runtime host to obtain information about the needs of the CLR concerning file mappings. To load an execute assemblies, the CLR uses the MapViewOfFile Win32 API function. It's clear that such an action to load an assembly (I'll cover assembly loading in a next post in this CLR Hosting episode on my blog) requires memory. As we want the host (think about SQL Server in particular if it helps to clarify stuff) to be able to get to know everything about memory allocations and releases, it's necessary for the CLR to report the allocation (and release) fo virtual address space (e.g. to count the total size of memory space used by the hosted CLR inside the host process). The CLR does this through the following functions:

    HRESULT NeedsVirtualAddressSpace([in] LPVOID       startAddress,
                                     [in] SIZE_T       size);


    HRESULT AcquiredVirtualAddressSpace([in] LPVOID       startAddress,
                                        [in] SIZE_T       size);


    HRESULT ReleasedVirtualAddressSpace([in] LPVOID       startAddress);

So, when the CLR uses MapViewOfFile it will call AcquiredVirtualAddressSpace to report this to the host and when the UnmapViewOfFile Win32 function was called, this is reported through ReleasedVirtualAddressSpace. The NeedsVirtualAddressSpace function gets called when a call of MapViewOfFile failed because of a low on memory condition. This gives the CLR Host a chance to make memory available in order to allow the CLR to retry this operation.

 

About garbage collection and hints to the CLR

This leaves us with two functions in the IHostMemoryManager interface that we didn't explain yet:

    HRESULT GetMemoryLoad([out] DWORD* pMemoryLoad,
                          [out] SIZE_T *pAvailableBytes);
   
    HRESULT RegisterMemoryNotificationCallback([in] ICLRMemoryNotificationCallback * pCallback);

The first function, GetMemoryLoad, is the equivalent in the CLR Hosting API for the Win32 function called GlobalMemoryStatus (more information over here). Both parameters of the CLR function are output parameters, which obviously means the host tells something to the CLR. The first parameter has to report a percentage of the memory load whileas the second one has to give the exact number of bytes that the CLR will still be able to allocate through the memory manager before an allocation error occurs. The CLR will call this function regularly to get a picture of the status of the memory managed by the host. These values are kept by the CLR to determine when the next round of garbage collection should be launched.

The second function is used by the CLR to pass through a pointer to a ICLRMemoryNotificationCallback object (remember the convention that interfaces started with ICLR are provided by the CLR, whileas the ones starting with IHost are provided by the host developer, that is you). The implementation of the function will be pretty straightforward, namely to "remember" the passed-in pointer in some global variable for later use. The functionality in the ICLRMemoryNotificationCallback interface is very limited:

interface ICLRMemoryNotificationCallback : IUnknown
{
    // Callback by Host on out of memory to request runtime to free memory.
    // Runtime will do a GC and Wait for PendingFinalizer.
    HRESULT OnMemoryNotification([in] EMemoryAvailable eMemoryAvailable);
}

Basically, what this OnMemoryNotification function allows the host to do is to tell the CLR about an out of memory condition (that is expected to happen "soon"). As a reaction on this reported status, the CLR can invoke the garbage collector to free memory. The possible values are declared in an EMemoryAvailable enumeration:

typedef enum
{
    eMemoryAvailableLow = 1,
    eMemoryAvailableNeutral = 2,
    eMemoryAvailableHigh = 3
} EMemoryAvailable;

Currently, only the first value (a low memory condition) will trigger the GC, but in the future the CLR will possible use the other values to drive the scheduling of the next GC round or so.

 

Hook in your memory manager

Assume you've written your memory manager in a class called MyHostMemoryManager (implementing the IHostMemoryManager interface). The next step is to tell the CLR about this during the initialization phase. To do this, you have to implement the IHostControl interface, for example in a class called MyHostControl. The function you need to implement is called GetHostManager.

HRESULT __stdcall MyHostControl::GetHostManager(REFIID id, void **ppHostManager)
{
   if (id == IID_IHostMemoryManager)
   {
      MyHostMemoryManager *pMemoryManager = new MyHostMemoryManager();
      *ppHostManager = (IHostMemoryManager*) pMemoryManager;
      return S_OK;
   }
   else
   {
      *ppHostManager = NULL;
      return E_NOINTERFACE; //tell the CLR we don't take care for the requested manager
   }
}

Now, the CLR is able to obtain an instance of the memory manager you've implemented. The startup code will have the following format:

ICLRRuntimeHost *pHost = NULL;
HRESULT res = CorBindToRuntimeEx(L"v2.0.40607", L"wks", STARTUP_CONCURRENT_GC, CLSID_CLRRuntimeHost, IID_CLRRuntimeHost, (PVOID*) &pHost);

assert(SUCCEEDED(res));

MyHostControl *pHostControl = new MyHostControl();
pHost->SetHostControl((IHostControl*) pHostControl);

This should do the trick. By calling SetHostControl, the CLR will start to ask your CLR host what responsibilities it wants to take. When asking for a memory manager, you'll return your memory manager object as explained above. As a reaction on this, the CLR will call the RegisterMemoryNotificationCallback function to offer you a pointer to the callback object you can use to report memory status to the CLR (through OnMemoryNotication). When pHost->Start() is called later on, the memory manager will be used to handle memory requests by the CLR.

 

Controlling the garbage collector

Controlling the garbage collector of the CLR can be done by two parties: the CLR and the Host. For this particular reason, two interfaces are present in mscoree:

interface ICLRGCManager : IUnknown
{
    /*
     * Forces a collection to occur for the given generation, regardless of
     * current GC statistics.  A value of -1 means collect all generations.
     */
    HRESULT Collect([in] LONG Generation);
   
    /*
     * Returns a set of current statistics about the state of the GC system.
     * These values can then be used by a smart allocation system to help the
     * GC run, by say adding more memory or forcing a collection.
     */
    HRESULT GetStats([in][out] COR_GC_STATS *pStats);
   
    /*
     * Sets the segment size and gen 0 maximum size.  This value may only be
     * specified once and will not change if called later.
     */
    HRESULT SetGCStartupLimits([in] DWORD SegmentSize, [in] DWORD MaxGen0Size);
}

interface IHostGCManager : IUnknown
{
    // Notification that the thread making the call is about to block, perhaps for
    // a GC or other suspension.  This gives the host an opportunity to re-schedule
    // the thread for unmanaged tasks.
    HRESULT ThreadIsBlockingForSuspension();

    // Notification that the runtime is beginning a thread suspension for a GC or
    // other suspension.  Do not reschedule this thread!
    HRESULT SuspensionStarting();

    // Notification that the runtime is resuming threads after a GC or other
    // suspension.      Do not reschedule this thread!
    HRESULT SuspensionEnding(DWORD Generation);
}

The ICLRGCManager is the easier one of both, because you don't have to implement it yourself. Instead, you can ask the ICLRControl object for the CLR-provided manager for garbage collector management. The mechanism to do this is pretty straightforward:

ICLRGCManager *pCLRGCManager = NULL;
res = pHost->GetCLRManager(IID_ICLRGCManager, (void**) &pCLRGCManager);

Once you have a reference to the ICLRGCManager object, you can use it to perform the following actions:

  • Collect - ask the GC to collect a certain generation (see the part 2 post of the CLR Hosting series for more information about the garbage collector generations model); use -1 to collect all of the generations. Note: in managed code you can call System.GC.Collect();
  • GetStats - pass in an object of type COR_GC_STATS telling the CLR which statistics - of the COR_GC_STAT_TYPES enumeration - you want (COR_GC_COUNTS and/or COR_GC_MEMORYUSAGE) and obtain the stats through the same object;
  • SetGCStartupLimits - control the segment size (>= 4 MB; multiple of 1 MB) and the size of generation 0 (>= 64 KB).

In part 2 of the CLR Hosting episode on my blog I told you a bit about the GC's need to suspend threads to reach a safe point and to kick in the garbage collection in a safe way. The CLI implementation of the garbage collector mentions the following in the gcee.cpp file:

// The contract between GC and the EE, for starting and finishing a GC is as follows:
//
//      LockThreadStore
//      SetGCInProgress
//      SuspendEE
//
//      ... perform the GC ...
//
//      SetGCDone
//      RestartEE
//      UnlockThreadStore

We're now talking about the SuspectEE (execution engine) and RestartEE steps:

void GCHeap::SuspendEE(SUSPEND_REASON reason)
{
    //...
    ThreadStore::TrapReturningThreads(TRUE);
    //...
    hr = Thread::SysSuspendForGC(reason);
}

void GCHeap::RestartEE(BOOL bFinishedGC, BOOL SuspendSucceded)
{
    //...
    ThreadStore::TrapReturningThreads(FALSE);
    //...
    Thread::SysResumeFromGC(bFinishedGC, SuspendSucceded);
    //...
}

In the introduction of this post I explained the need of SQL Server to be able to control almost everything through its own "SQL OS" layer. This "everything" includes thread and synchronization management. If a host (such as SQL Server 2005) has this need, it's likely it wants to know about the thread manipulation that's performed by the CLR's GC to be able to (reach a safe point and) run. For that purpose, a host can provide a IHostGCManager implementation, providing three functions:

  • ThreadIsBlockingForSuspension - Notify the host about the fact that the thread that's calling this method will be suspended soon to perform some work (e.g. to perform a garbage collection). This is what the SysSuspendForGC function is called for. As explained in the comments for the function, this allows the host to perform scheduling work to use resources as effectively as needed (which is certainly the case of SQL Server 2005 because of performance and scalability needs as explained earlier).
  • SuspensionStarting - The thread suspension is starting now.
  • SuspensionEnding - Tells the host that suspension of the thread is stopping, also telling the host which generation was garbage collected during the thread's suspension.

 

Conclusion

The CLR Hosting APIs allow you as a developer of a CLR host to control virtually any aspect of memory management in relation to the CLR. You can take advantage of this to control memory limits, to track memory usage for statistics and performance tuning, and so on.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Introduction

In the previous posts on CLR Hosting with .NET v2.0 I explained the basic principles of - guess what - hosting the CLR inside a process by using the mscoree library. This post will explain what the various options are for starting the CLR and how the basic lifecycle of a hosted CLR will look like. In the next episodes we'll dive into more detailed stuff on how to customize the hosted CLR in much more detail.

 

Basic code skeleton for CLR hosting

In part 1 of this episode, I showed the following code fragment to launch the CLR:

ICLRRuntimeHost *pHost = NULL;
HRESULT res = CorBindToRuntimeEx(L"v2.0.40607", L"wks", STARTUP_SERVER_GC, CLSID_CLRRuntimeHost, IID_CLRRuntimeHost, (PVOID*) &pHost);
assert(SUCCEEDED(res));

res = pHost->Start();
assert(SUCCEEDED(res));

Let's now explain in somewhat more detail what the various parameters of the CorBindToRuntimeEx function mean and how you can use these parameters to customize the behavior of the CLR.

 

CLR versioning

Being in the heart of the .NET Framework, the CLR is upgraded in every release of the .NET Framework and thus there should be a versioning concept when talking about the CLR as such. In order not to break existing applications, the .NET Framework supports side-by-side installation of different versions of the CLR (and the .NET Framework BCL assemblies in the GAC). The various versions of the CLR on your machine can be found in the Windows\Microsoft.NET\Framework folder on the system, where every subfolder contains a specific version of the .NET Framework (e.g. 1.0.3705, 1.1.4322). Furthermore, there's a key in the registry that enumerates all of the installed versions of the .NET Framework. This key can be found in the registry under HKLM\SOFTWARE\Microsoft\.NETFramework\Policy. However, although you can have multiple versions of the .NET Framework and CLR on the same machine, there will be only one startup shim on the machine, with the name mscoree.dll. This DLL is using the registry to locate the .NET Framework installation files for the requested CLR version and to hand over the execution control to the requested CLR engine which is installed in one of the Windows\Microsoft.NET\Framework subfolders.

When loading a specific version of the CLR, the shim still has a choice to make: whether to load the server build or workstation build, as explained in the next paragraph. However, as the implementer of a CLR Host you do have to make a decision too, which is the version loading strategy you want to follow:

  • A first choice is to go hand in hand with a certain version of the CLR, which is in my opinion the best possible choice. SQL Server 2005 uses this approach and will always load the version of the CLR that it was originally built with. I can't give a version number yet, as the final builds of SQL Server 2005 and the .NET Framework v2.0 are not available yet. But if you even wondered why the SQL Server 2005 and .NET Framework v2.0/Visual Studio 2005 releases are so tightly bound to each other, this is the reason why: in order to finalize the SQL Server 2005 development and make the RTM build, the v2.0 build of the CLR and .NET Framework has to be frozen first.
  • The other option is to load the latest version of the CLR that's available on the system. I won't cover this because I do not prefer this strategy as it can lead to compatibility problems (although this should be reduced to a minimum, it's better to stick with a specific version of the CLR to avoid this kind of problems proactively).

As a result, the first parameter of the CorBindToRuntimeEx function contains the version number in the format "v<major>.<minor>.<build>". Tip: also look at CorBindToCurrentRuntime which uses a configuration file to retrieve information about which version of the CLR to load.

Warning: The version which will be loaded does not have to match the one you specify, because of possible policy configuration on the machine. You can force the shim to load the specified version "literally" by using the STARTUP_LOADER_SAFEMODE flag in the third parameter of CorBindToRuntimeEx.

If not implementing a custom CLR Host, you can use the app.exe.config file to configure a specific version to be loaded. If multiple supportedRuntime-elements are specified, evaluation occurs from top to bottom. A requiredRuntime-element is only needed for backward compat on machines that only do have version 1.0.3705 of the .NET Framework, so I do not mention it in the configuration file below.

<configuration>
   <startup>
      <supportedRuntime version="v<major>.<minor>.<build>" /> <!-- specify version that is supported by app-->
      ...
   </startup>
</configuration>

 

Which build?

The CLR always ships with two different builds of the core execution engine, being the workstation build (mscorwks.dll) and the server build (mscorsvr.dll). Now, what's the difference between the two? Let's start with the begin: one of the key architectural elements of the CLR is the garbage collector for automatic memory management (in contrast to unmanaged code, e.g. in C with malloc/free or C++ with new/delete). Now, how does garbage collection work? An intermezzo...

<INTERMEZZO Title="Garbage collection in the .NET Framework">

The lifecycle of an object in the CLR is pretty straightforward. First of all memory has to be allocated, which is done by the newobj instruction in MSIL-code that is accessibly through the new keyword in C# and similar keywords in other managed languages. This operator is mapped on low level calls to allocate memory, just as the malloc function in C has to do to obtain memory. Next, the object has to be initialized which is done by calling the constructor of the type. After these two initial steps, the object is ready to be used and starts its lifetime in the hard .NET world :-). Now, at a certain point in time the object is not longer needed. When this is the case, the developer can optionally (!) explicitly (or implicitly) indicate that the object is no longer needed by calling the (optional) Dispose method on the object (cf. the IDisposable pattern and the using keyword in C#). This phase allows clean-up of the allocated resources and the state embedded in the object itself. Once this phase (if needed) has completed, it's time to free the memory which is done by the garbage collector.

This leaves us with some questions. The first one is how the CLR does allocate memory when the newobj instruction is called. First of all, remark that newobj is kind of an object oriented virtual machine language instruction, so in the end it has to mapped on a low-level processor instruction asking for n bytes of memory. Therefore, the CLR starts by calculating how much space is needed on the managed heap to hold the object and additional information that the CLR uses to do housekeeping stuff. Basically, the managed heap looks like an array of objects sitting next to each other and a pointer (NextObjPtr) that indicates the next position where an object can be allocated on the heap. This mechanism allows fast memory allocation, as there is no need to a list traversal to find free memory. Memory in the managed environment thus has a contigious look-n-feel. However, what I told you so far is pretty wishful thinking: the available memory isn't infinite so at a certain point in time newobj will come to the conclusion that there is no address space left to allocate the object because the heap is full (NextObjPtr + n bytes > end of address space). So far for the easy stuff :-).

The CLR has a so-called ephemeral garbage collector which means that objects are grouped in generations that are related to an object's lifetime. Basically this means that newly allocated objects will be living in generation 0. Assume the CLR is running for a while now and 10 objects have been created of which 4 are not longer needed. In comes a request for memory allocation on the managed heap through a newobj call. The CLR however has a built-in threshold value for the size of generation 0. Assume this threshold has been reached and therefore it's not possible to allocate memory directly for the new-to-be-created object in generation 0. At this point in time, the garbage collector comes into play. It takes a look at the objects in the managed heap and concludes that 4 of these objects are not longer needed. Assume these are objects 3,6,7,8, then the managed heap will be compacted to 1,2,4,5,9,10. When the garbage collector has finished its job, these objects no longer belong to generation 0 but are moved to generation 1. As a result, generation 0 is now empty (NextObjPtr is reset to the initial position in generation 0) and the newobj call can continue (aforementioned condition for available address space is met). Collection generation 0 generally reclaims enough memory to continue and will be quite effective because generation 0 is rather small (and therefore analysis and garbage collection goes fast) but also because of the fact that a lot of objects do only live for a short time. If objects survive this collection, they end up in generation 1 that consists of objects that have a longer lifetime. It's however not difficult to see that generation 1 will grow too, therefore it has a threshold too. When this situation occurs (triggered by a full generation 0 and an unsuccessful move of generation 0 objects to generation 1), generation 1 (which is larger than generation 0) will be collected too. Objects that survive this garbage collection process will move to generation 2 (the last generation in the CLR). When this is done, generation 0 is analyzed and collected, promoting the survivor objects to generation 1. It's clear that a generation 1 level collection takes more time than a generation 0 level collection, but also that collections of generation 1 will occur less frequently than the collections of generation 0. In the end, the characteristics of an ephemeral garbage collector can be explained by the very basic assumption that newly created objects are likely to have a short lifetime and old (surviving) objects are likely to live longer. When garbage collection can't free any memory, the CLR will throw an OutOfMemoryException.

Note the whole garbage collection mechanisme is far more complex than what I did describe above. A few points to take care of include:

  • The garbage collector changes the addresses of objects in memory. Therefore, thread safety is a must so that other parties do not access wrong memory locations when garbage collection is being performed. As the matter in fact, all managed code threads have to be suspended before the garbage collector can start its mission. In order to do this safely, the CLR has to take track of a lot of things in order to make sure the suspension does not hurt the thread when it has to be resumed after the garbage collection took place. This is based on so-called safe points. If a thread does not reach a safe point in a timely fashion, the CLR will perform a trick called thread hijacking to modify the thread's stack. To make things even more complicated, unmanaged code needs special treatment in some cases too. This would bring us too far, so I do refer to the book "Applied .NET Framework Programming" for more information about this.
  • Large objects (larger than about 85 KB) are allocated on a separate large object heap and start their lifecycle in generation 2. The reason for this special treatment is to reduce shifting of large memory blocks when performing garbage collection. As a result, it's recommended to use large objects only when these are long-lived.
  • Objects that are collected during a garbage collector run can have a finalizer (in C# defined by a destructor-like syntax; e.g. ~MyObject). Such an object has a Finalize method that should be called when the object is deleted and ends its lifecycle. The garbage collector uses a finalization list to determine whether an object needs finalization first. If that's the case, the object's pointer is put on a freachable queue (f stands for finalization). Such an object is not (!) considered to be garbage (yet) because it's still reachable (as the name tells us). A special thread in the CLR checks this queue on a regular basis to finalize the objects that are listed in there. During the next run of the garbage collector it can be determined which objects have now become real garbage because the finalization took place and the objects have disappeared from the freachable queue.

In this discussion I made one big assumption: the CLR knows which objects are not needed anymore. How does it do that? The mechanism that allows the CLR to do this is based on the concept of a root, which is "some location" that contains a memory pointer to an object. Examples are global variables, static variables, variables on the current thread stack and CPU registers that point to a reference type. When the Just-In-Time compiler does its work, it maintains an internal table which maps begin and end code offsets (with code I do mean native code, the result of jitting) to the root(s) of the method that's executing. During the execution of the native code, the garbage collector can be called (because a running-out-of-memory condition as explained above). It's clear that this will happen at a certain code offset. Each offset is embeddded in a region between a begin offset and end offset. By looking in the table created by the JIT compiler, a set of roots can be found. Beside of this table information we also do have thread stack information when the execution is interrupted by the garbage collecting process. Using the thread's call stack, the garbage collector can perform a thread stack walk to find the roots for all of the calling methods, again by using internal tables constructed by the JIT compiler at runtime for each method. Once this information is known, the garbage collector can create a reachable objects graph that is used to find out which objects are still needed. When recursing through this graph, object that are still in use are marked. Any unmarked objects after this phase are considered to be garbage (as these are not reachable starting from a root anymore) and therefore can be collected. In the last phase, the garbage collector walks over the managed heap and looks for large (to avoid little memory shifts that wouldn't give much of gain to the end result) contiguous regions of free space (i.e. unmarked garbage objects). When such a region is found, the garbage collector compacts the heap by shifting the objects in memory. When doing this, the roots are updated because memory addresses of the moved objects have changed of course.

I'll tell more about garbage collector in a later post when talking about memory management in CLR Hosting.

</INTERMEZZO>

Okay, now you should have some picture of how garbage collection works. Back to the difference between the server and workstation builds of the CLR. In the intermezzo I explained how threads have to be suspended so that the garbage collector (thread) can kick in to do its job. On server machines (I'll define the term "server" in a moment) we like to minimize the overhead of this garbage collection as much as we can. Assume you have a machine with multiple processors. In that case it's possible to run garbage collections in parallel on the machine. This is exactly what the server build supports. By default the workstation build will be loaded and the server build can't even be loaded when you don't have a multiprocessor machine. This explains the second parameter of the CorBindToRuntimeEx function. It can take the following two values:

  • wks
  • svr

Now, the third parameter of the CorBindToRuntimeEx function is related to the garbage collector too. For the workstation build, there is support for two different modi to run the garbage collector in. The first is the concurrent mode (STARTUP_CONCURRENT_GC) that will work on multiprocessor machines (but we're not running the server build of the CLR, remember that, we're talking about the workstation build specifically). In this mode, collections will happen concurrently in a background thread while the foreground threads are working. On a uniprocessor machine, collections happen on the same threads as the foreground code. Nonconcurrent mode does the collections in the same threads as the foreground code. The server build always uses nonconcurrent mode and nonconcurrent mode while running the workstation build is the recommended value for non UI-intensive apps (e.g. SQL Server 2005 uses nonconcurrent mode).

Warning: there is a trick to load the server build on non-supported configurations (see above) by specifying the "svr" parameter in combination with concurrent mode.

The default of concurrent collection in workstation builds is a nice one. If you want to disable this without going through the process of creating a full CLR Host, you can use a configuration file (<app.exe>.config) to specify the garbage collector's behavior in relation to this:

<configuration>
   <runtime>
      <gcConcurrent enabled="..." /> <!-- true is the default; useful when running in workstation build (default) to turn concurrent collection off, e.g. in batch processing apps -->
      <gcServer enabled="..." /> <!-- set to true to load the server build (not the default); however, if non on a multiproc, the workstation build will be loaded instead -->
   </runtime>
</configuration>

 

Domain-neutral code introduced

A last concept for now is the concept of domain-neutral code, of which the behavior can be set through the third parameter of CorBindToRuntimeEx too. The three possible options are:

STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN // no domain neutral loading
STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN // all domain neutral loading
STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST // strong name domain neutral loading

Now, what is domain-neutral code? Let's start with a refresh of the mechanism of DLLs in Windows. DLL stands for Dynamic Linked Library and contains a library of code that can be used by various applications on the machine. The good is the idea, the bad and the ugly is the DLL Hell as a result of versioning troubles. However, the concept is fine in a sense that when multiple applications use the same DLL at the same time, it's possible for the OS to keep the instructions of the dll only once in memory, therefore reducing the working set of the applications that use it. One copy of the code (which does not change) is sufficient for all apps that are dependent on it to execute.

Domain-neutral code is the equivalent in .NET of this kind of sharing of common code across multiple dependent applications. In managed code, things are a little more complicated however due to the JIT compiler. CLR Hosting allows you to customize the behavior of domain-neutral assembly code loading thorugh the startup parameters and through the implementation of the IHostControl interface which I'll explain later on in this episode. The three possible startup parameters dictate the CLR to disable all domain neutral loading (except for mscorlib, the core of the class library, which is always domain-neutral) or to enable domain neutral loading for all assemblies or some way in between based on strong named assemblies. In fact, all assemblies that are to be loaded domain-neutrally have to form a "closure", that is all referenced assemblies of a given domain-neutral loaded assembly have to be loaded domain-neutral too. These three default startup parameter values follow this rule.

Q&A: Why are not all assemblies loaded domain-neutrally to reduce the overall working set as much as possible? The answer is that once an assembly is loaded domain-neutrally, it can't be unloaded anymore without unloading the appdomain and the entire process. It's clear that this behavior is not desirable for CLR hosts such as ASP.NET or SQL Server 2005, where it should be possible to replace an assembly without restarting the server (service) to free resources.

 

Starting and stopping the CLR

The call to CorBindToRuntimeEx actually initializes the CLR. The result of this is a return parameter of the type ICLRRuntimeHost that can be used for further interaction with the CLR. The first call you'll make is a call to the method Start to start the mission of the CLR in your process. Once this is done, there is no real way back. Although you can stop the CLR by calling the method Stop, it's not possible to restart the CLR. Neither is it possible to completely unload the CLR from the process. Once the CLR has been in a process, it can't be reinitialized or restarted without creating a new process.

 

Delay load

To finish this post on CLR Hosting basics, I want to tell you something about delay loading. It's clear that loading the CLR takes some time to complete and maybe you come to the conclusion you have been doing this work for nothing, because during the lifetime of the process no managed code has to be executed. Suppose for example you want to offer managed code support in a database engine. It would be a waste of resources to load the CLR always by default when the database engine starts if nobody needs it later on. Instead, it would be nicer to be able to load the CLR when it's needed to do so (e.g. because of a COM component that calls managed code). For that purpose the CLR Hosting API provides some mechanisms to defer loading.

The first (easy) way to defer loading is to prepare the loading but wait until it's actually needed. This is called deferred startup and can be initiated by using the STARTUP_LOADER_SETPREFERENCE flag as the third parameter of the CorBindToRuntimeEx function. The only thing this does is saving the passed value for the version parameter. When the CLR needs to be loaded, that version will be used (e.g. by calling CorBindToRuntimeEx a second time). This is however very limited in a sense that you can't control other startup parameters for the CLR.

To support that, a function called LockClrVersion is available in the startup shim (see mscoree.IDL) that takes a callback to a function that needs to be called when the real initialization takes place, giving you as a host a chance to manipulate various settings. The signature looks as follows:

STDAPI LockClrVersion(FLockClrVersionCallback hostCallback,FLockClrVersionCallback *pBeginHostSetup,FLockClrVersionCallback *pEndHostSetup);

The first parameter is very straightforward. The two next parameters contain pointers to callback functions (provided to us by the shim) that are to be called before and after initialization to allow housekeeping by the CLR to know which thread is the owner of the initialization to control the overall (exclusively-granted-to-one-thread) loading process and to block any managed code requests that could interfere with this. Skeleton code for lazy CLR loading takes the following form:

FLockClrVersionCallback begin_init, end_init;

STDAPI init()
{
   //we're in control; notify the shim to grant us the exclusive initialization right
   begin_init();

   //CorBindToRuntimeEx stuff goes here

   //mission completed; tell the shim we're ready
   end_init();
}

void prepare_init()
{
   //tell the shim we want to take control when needed
   LockClrVersion(init, &begin_init, &end_init);

   //wait till something happens that causes the CLR to be loaded
}

 

Conclusion

Initialization and startup of the CLR is fully customizable as you saw in this post, even in a delayed loading scenario. In the next posts I'll show how to control the behavior of the CLR even further using self-written CLR Hosting API implementations.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Introduction

This is the first real post in my "CLR Hosting" blogseries that's coming up in the next couple of weeks (or months?). CLR Hosting is in my opinion a great API in a sense that it allows third parties to integrate the CLR in their products. Well, that's completely true of course, but what's the value "normal" developers? My personal answer to this question is: you can learn a lot of how the CLR actually works by taking a look at this API. It also helps you to understand how SQL Server 2005 can adopt the CLR in the database engine in such a way that the CLR behaves in exactly the way the SQL OS folks want it to behave (compare with "parental control").

 

The CLR's execution engine

Let's start by taking a look at the basics of the CLR when it comes to executing code. The central file in this story is mscoree.dll (Component Object Runtime Execution Engine), which contains the execution engine. Well, that's not completely true actually. Mscoree is called the "startup shim" and is unique on the machine, regardless of the number of side-by-side installations of the .NET Framework (e.g. 1.0.3705, 1.1.4322, 2.0.x). You'll find the file in the system32 folder on your system. It's the task of the mscoree.dll file to hand over execution to a specific version of the CLR depending on a number of factors. Such an installation of a version of the CLR contains a bunch of files starting with mscor, such as:

  • mscorwks.dll - the workstation version of the CLR
  • mscorsvr.dll - the server version of the CLR (I'll talk about the workstation and server versions in a later post when talking about the garbage collector etc)
  • mscorlib.dll - contains a part of the System namespace of managed classes (e.g. System.Activator is in there, whileas System.Uri lives in the System.dll assembly); this file contains low-level functionality that has a close relationship with the CLR itself (e.g. code to support concepts such as application domains)
  • mscorjit.dll - the just-in-time compiler of the CLR to compile IL-code to native code at runtime

You can find all these files in the Microsoft.NET\Framework folders in your Windows directory. As you can have multiple different versions of the CLR on one machine, it's the job of the startup shim (which is not installed on a version-per-version basis) to load a specific version of the CLR and to hand over execution to that particular version.

Now, how does the CLR get loaded when a managed assembly is started? The answer depends on the operating system you're running. Let's start at the end of the story: mscoree.dll contains an "entry-point" for managed execution of an assembly, called _CorExeMain (and _CorDllMain). This function has to be called to hand over execution of a managed assembly (which is wrapped inside a standard PE - portable execution format - file) to the CLR. Machines with Windows XP and Windows Server 2003 know how to recognize a managed assembly and call this function directly when such an assembly is loaded by the PE operating system loader. On other versions of the Windows operating system, a small launch routine is inserted in the PE-file to hand over control to the CLR, by calling the _CorExeMain function. You can find another post on this subject on http://blogs.wwwcoder.com/rajaganesh/archive/2005/06/30/5386.aspx. To find out about a PE file containing managed code, XP and W2K3 (and later) check the "COM Descriptor Directory" entry in the file header. You can take a look at this information yourself by using the dumpbin tool with the switch /headers:

C:\Documents and Settings\BartDS>dumpbin /headers hello.exe
Microsoft (R) COFF/PE Dumper Version 7.10.3077
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file hello.exe

PE signature found

File Type: EXECUTABLE IMAGE

FILE HEADER VALUES
             14C machine (x86)
               2 number of sections
        422F31FB time date stamp Wed Mar 09 18:27:23 2005
               0 file pointer to symbol table
               0 number of symbols
              E0 size of optional header
             10E characteristics
                   Executable
                   Line numbers stripped
                   Symbols stripped
                   32 bit word machine

OPTIONAL HEADER VALUES
             10B magic # (PE32)
            8.00 linker version
             400 size of code
             200 size of initialized data
               0 size of uninitialized data
            22DE entry point (004022DE)
            2000 base of code
            4000 base of data
          400000 image base (00400000 to 00405FFF)
            2000 section alignment
             200 file alignment
            4.00 operating system version
            0.00 image version
            4.00 subsystem version
               0 Win32 version
            6000 size of image
             200 size of headers
               0 checksum
               3 subsystem (Windows CUI)
             400 DLL characteristics
                   No safe exception handler
          400000 size of stack reserve
            1000 size of stack commit
          100000 size of heap reserve
            1000 size of heap commit
               0 loader flags
              10 number of directories
               0 [       0] RVA [size] of Export Directory
            228C [      4F] RVA [size] of Import Directory
               0 [       0] RVA [size] of Resource Directory
               0 [       0] RVA [size] of Exception Directory
               0 [       0] RVA [size] of Certificates Directory
            4000 [       C] RVA [size] of Base Relocation Directory
               0 [       0] RVA [size] of Debug Directory
               0 [       0] RVA [size] of Architecture Directory
               0 [       0] RVA [size] of Global Pointer Directory
               0 [       0] RVA [size] of Thread Storage Directory
               0 [       0] RVA [size] of Load Configuration Directory
               0 [       0] RVA [size] of Bound Import Directory
            2000 [       8] RVA [size] of Import Address Table Directory
               0 [       0] RVA [size] of Delay Import Directory
            2008 [      48] RVA [size] of COM Descriptor Directory
               0 [       0] RVA [size] of Reserved Directory


SECTION HEADER #1
   .text name
     2E4 virtual size
    2000 virtual address (00402000 to 004022E3)
     400 size of raw data
     200 file pointer to raw data (00000200 to 000005FF)
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         Execute Read

SECTION HEADER #2
  .reloc name
       C virtual size
    4000 virtual address (00404000 to 0040400B)
     200 size of raw data
     600 file pointer to raw data (00000600 to 000007FF)
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
42000040 flags
         Initialized Data
         Discardable
         Read Only

  Summary

        2000 .reloc
        2000 .text

C:\Documents and Settings\BartDS>

Make sure you're using the 7.0 version of dumpbin as in earlier versions the "COM Descriptor Directory" is still called "Reserved Directory" and there are a couple of these reserved directory entries in the list :-). The 7.0 (and higher) versions also have a switch /CLRHEADER that is useful to display the CLR-header that's embedded in a PE file:

C:\Documents and Settings\BartDS>dumpbin /clrheader hello.exe
Microsoft (R) COFF/PE Dumper Version 7.10.3077
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file hello.exe

File Type: EXECUTABLE IMAGE

  clr Header:

              48 cb
            2.00 runtime version
            2068 [     224] RVA [size] of MetaData Directory
               1 flags
         6000001 entry point token
               0 [       0] RVA [size] of Resources Directory
               0 [       0] RVA [size] of StrongNameSignature Directory
               0 [       0] RVA [size] of CodeManagerTable Directory
               0 [       0] RVA [size] of VTableFixups Directory
               0 [       0] RVA [size] of ExportAddressTableJumps Directory


  Summary

        2000 .reloc
        2000 .text

C:\Documents and Settings\BartDS>

On a non-managed file, you won't get any information other than the summary when running a dumpbin /clrheaders against the file.

 

Introduction to the CLR Hosting API

Now it's time to JMP to the real stuff. As I've explained, mscoree.dll is responsible to load a specific version of the CLR and thus to tell that particular version how to initialize. The mscoree.dll version will always be the version of the most recent CLR version running on your system and has to maintain things as backward compatible as possible as the file is subject to the old "COM DLL Hell" (it resides in system32 and has to be registered on the system). In the bin\include subdirectory of your SDK installation folder of that particular most recent version of the .NET Framework, you'll find a mscoree.idl file that contains the public export information of the functions inside the library. For version 2.0 of the .NET Framework you'll find a section marked with:

//*****************************************************************************
// New interface for hosting mscoree
//*****************************************************************************

The stuff in this section will be the subject of this and upcoming posts in the "CLR Hosting" series, i.e.:

interface ICLRRuntimeHost : IUnknown

The functions that go in there will be explained later on, but some of these should look familiar: Start, Stop, ExecuteApplication, etc. Others give access to the IHostControl object that can be used to configure the CLR prior to startup. The general principle of writing a CLR Host is implementing interfaces that start with IHost. These implementations contain your code to tell the CLR how to behave and allow you to control the overall behavior of the CLR that you want to control in your specific scenario. Examples are assembly loading, memory management, threading and locking, and so on. A more complete list follows. On the other hand there are a bunch of interfaces that start with ICLR which indicates that the CLR itself is responsible to provide an implementation. So, how do we get an instance of an ICLRRuntimeHost object to kick off with our hosting stuff? The answer is CorBindToRuntimeEx, which is the function to load the CLR in a process. Open up mscoree.h and you should find this line:

STDAPI CorBindToRuntimeEx(LPCWSTR pwszVersion, LPCWSTR pwszBuildFlavor, DWORD startupFlags, REFCLSID rclsid, REFIID riid, LPVOID FAR *ppv);

The first parameter takes the version (e.g. L"v2.0.40607"), the second one the build flavor being workstation or server (e.g. L"wks" for the workstation build, more information follows later when talking about the GC), next we have a DWORD variable for startup flags of the following enum:

// By default GC is non-concurrent and only the base system library is loaded into the domain-neutral area.
typedef enum {
  STARTUP_CONCURRENT_GC         = 0x1,

  STARTUP_LOADER_OPTIMIZATION_MASK = 0x3<<1,                    // loader optimization mask
  STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN = 0x1<<1,           // no domain neutral loading
  STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN = 0x2<<1,            // all domain neutral loading
  STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST = 0x3<<1,       // strong name domain neutral loading


  STARTUP_LOADER_SAFEMODE = 0x10,                               // Do not apply runtime version policy to the version passed in
  STARTUP_LOADER_SETPREFERENCE = 0x100,                         // Set preferred runtime. Do not actally start it

  STARTUP_SERVER_GC             = 0x1000,                       // Use server GC
  STARTUP_HOARD_GC_VM           = 0x2000,                       // GC keeps virtual address used
  STARTUP_LEGACY_IMPERSONATION             = 0x10000,                        // Do not flow impersonation across async points by default
} STARTUP_FLAGS;

I'll explain the difference between the concurrent_gc and server_gc later on. The next two parameters take some information about the runtime host, more specifically the CLSID and the IID of the interface:

EXTERN_GUID(CLSID_CLRRuntimeHost, 0x90F1A06E, 0x7712, 0x4762, 0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x01);
EXTERN_GUID(IID_ICLRRuntimeHost, 0x90F1A06C, 0x7712, 0x4762, 0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x01);

Finally, the last parameter is the one you need to obtain an object to work with. It's a pointer to an object that will contain the ICLRRuntimeHost instance that can be used to do, well all of the stuff that's mentioned in the IDL definition for this interface. So, the way to use it is to pass in an address of a pointer of the type ICLRRuntimeHost*, with some casting (C++ you know :-)). The overall result with my installation of the .NET Framework v2.0 looks like this:

ICLRRuntimeHost *pHost = NULL;
HRESULT res = CorBindToRuntimeEx(L"v2.0.40607", L"wks", STARTUP_SERVER_GC, CLSID_CLRRuntimeHost, IID_CLRRuntimeHost, (PVOID*) &pHost);

Once this code has been executed the CLR has been loaded in the process space and is ready to be launched but still waiting on a Start command. Before calling start you can actually manipulate the CLR's settings as I'll explain in much more detail later on. To start the CLR, just call Start:

pHost->Start();

 

Who's implementing what?

Time for the discovery phase. In the previous section I explained briefly that it's possible to take responsibility for certain CLR-related functionality by implementing certain interfaces. This way, you just implement what you need. For example, you might need more control over memory allocation whileas you don't need to control threading in your scenario. This flexibility results in another complexity however and that's the problem of discovery: finding out who implements what. For example, the CLR needs to know what responsibilities the host wants to take over. For that purpose, there's an interface called IHostControl:

interface IHostControl : IUnknown
{
    HRESULT GetHostManager(
        [in] REFIID riid,
        [out] void **ppObject);

    /* Notify Host with IUnknown with the pointer to AppDomainManager */
        HRESULT SetAppDomainManager(
        [in] DWORD dwAppDomainID,
        [in] IUnknown* pUnkAppDomainManager);

    HRESULT GetDomainNeutralAssemblies(
        [out] ICLRAssemblyReferenceList **ppReferenceList);
}

We're interested in the first method, being GetHostManager. It's your task as a CLR hosting developer to implement this interface and this method. When the CLR is started and the host control is bound to the CLR runtime host object, the CLR initiates a dialog based on a series of IIDs for everything the host can take responsibility for. To put this in simple words, a dialog like this is going on:

  • Host to CLR runtime host (pHost): here's the host control object (MyHostControl)
  • CLR to host control object (MyHostControl) via method GetHostManager:
    • Hi there, do you implement a memory manager? If so, please give me a reference to it?
    • Hi there, do you implement a garbage collector manager? If so, please give me a reference to it?
    • Hi there, do you implement a thread pool manager? If so, please give me a reference to it?
    • and so on...

What you have to do, is implementing the IHostControl interface and give an implementation for the GetHostManager method that looks like this:

HRESULT __stdcall MyHostControl::GetHostManager(REFIID id, void **ppHostManager)
{
   if (id == IID_IHost...Manager)
   {
      MyHost...Manager *p...Manager = new MyHost...Manager(); //implements IHost...Manager

      //other stuff to initialize the manager

      *ppHostManager = (IHost...Manager*) p...Manager;
      return S_OK;
   }
   else if (id == IID_IHost...Manager)
   {
      //same story over here for this particular manager
   }
   else if (id == IID_IHost...Manager)
   {
      //same story over here for this particular manager
   }
   else
   {
      *ppHostManager = NULL;
      return E_NOINTERFACE; //tell the CLR we don't take care for the requested manager
   }
}

In this case, initialization looks as follows:

ICLRRuntimeHost *pHost = NULL;
HRESULT res = CorBindToRuntimeEx(L"v2.0.40607", L"wks", STARTUP_SERVER_GC, CLSID_CLRRuntimeHost, IID_CLRRuntimeHost, (PVOID*) &pHost);
MyHostControl *pHostControl = new MyHostControl();
pHost->SetHostControl((IHostControl*) pHostControl);

If you as a host want to know what the CLR's managers are for various tasks, the process works in a similar fashion. First, you do obtain a reference to the ICLRControl (just substitute Host with CLR and you're usually right):

ICLRControl pCLRControl = NULL;
pHost->GetCLRControl(&pCLRControl);

Next, you use the IID_ICLR...Manager values to ask the CLR for a particular manager. You'll find the complete list of managers in the mscoree.idl file when looking for IID_ICLR...Manager values in there. The skeleton looks like this:

ICLR...Manager *p...Manager = NULL;
pCLRControl->GetCLRManager(IID_ICLR...Manager, (void**) &p...Manager);

I won't give an overview of the various managers you can decide to implement, as I'll focus on these individually later on.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Introduction

In this upcoming series of blogposts on "CLR Hosting" I'm going to tell you how you can take benefit of the .NET Framework v2.0's CLR Hosting API in order to take advantage of the power of the CLR inside your own applications. Together with a series on ".NET Framework internals" I have the intention to offer you a collection of posts that answer some common questions I did receive in the last couple of months/years and that I've asked myself too before I decided to dive into this stuff a little deeper.

The stuff I'll be covering here is in fact an aggregation of various sources I've consulted myself while learning the "dark sides of .NET". Without doubt, much of this stuff will be covered elsewhere on the world wide web too, but by writing a series of posts over here I've the feeling to deliver added value by writing a series of mini-articles that give a pretty good overview of the overall structure and functionality of the .NET Framework and the CLR, that can be consulted by myself and others as a quick reference. As the matter in fact, I'll be creating some level-400 resources (PowerPoints, demos) for myself as I had to perform quite a lot of "cut everywhere on the web-paste in one ppt" methodology while composing presentations for .NET introductions and .NET dive deeper sessions.

 

Must-reads

As mentioned above, you have to look at these blogpost series as being aggregations of various sources. I consider some of these as mandatory reading for everyone who's working with .NET as a developer and wants to know more than "how to write code, how to compile it, how to debug it and how to run it", including:

Other resources include:

I'll update this list from time to time too.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

More Posts