< C++ .Net System Programming 7 | Main | Old To New C++ .Net 1 >


 

 

Early Stages of the C++ .Net 22

(Managed Extensions for C++)

 

 

The following are the topics available in this page.

  1. Verifiable Code

  2. Unmanaged .NET Services API

  3. Enumerating Managed Processes

  4. Getting Information About the Garbage Collector

  5. Hosting the .NET Runtime

  6. Initializing the Runtime

  7. Summary

 

 

 

 

Verifiable Code

 

.NET is type-safe, so to call a type member, the MSIL will have the metadata of the type and the member. MSIL is stack-based, and .NET compilers that generate the MSIL should construct the stack correctly before calling a type member. When your code is loaded by the runtime, the code is verified to see that it is type-safe and also that the code does not do anything else that is unsafe, such as access unmanaged memory or get direct access to the managed heap through an interior pointer. If the code fails this verification, it will not run in trusted situations. Furthermore, the code has to have the SkipVerification security permission request to run at all. All assemblies created by the C++ compiler have this security permission request. The .NET Framework provides a tool named peverify that you can use to check whether an assembly has type-safe code. Even C++ assemblies that do not perform unsafe actions will fail verification with peverify.

 

The .NET Framework provides a tool named peverify that you can use to check whether an assembly has type-safe code

 

Figure 25

 

Code that is downloaded from another machine must be verifiable before it can be run. Clearly, allowing non-verifiable code to run on your machine is not desirable because the code could bypass .NET security and type safety, access managed memory and manipulate the stack. Code that is not verifiable will not be loaded if its source is another machine. This means that you cannot use C++ to write assemblies intended to be loaded from another machine.

 

Unmanaged .NET Services API

 

Many parts of the .NET Framework are exposed to external code. Clearly, if you want to get information about the runtime or affect how the runtime works, this task should be done from outside the runtime, from non-.NET code. The .NET Framework SDK contains definitions of COM interfaces and objects that give access to the runtime. The header files that contain these definitions are shown in Table 5.

 

Header

Description

cor.h

Main header file for the metadata APIs

cordebug.h

Main .NET debugger interfaces

corerror.h

Definitions of the HRESULTs that can be returned from the runtime

corhdr.h

Definitions of the metadata structures

corhlpr.h

Helper functions

corprof.h

Profiling interfaces

corpub.h

Access to the list of running .NET processes

corsvc.h

Services for .NET debuggers

corsym.h

API to read and write debugging symbols

gchost.h

Gives access to statistics about the garbage collector

iceefilegen.h

API for generating .NET files

icmprecs.h

Access to the .NET data storage layer

ivalidator.h, ivehandler.h

API to validate .NET files

mscoree.h

Main header file for hosting the runtime

strongname.h

Header for the APIs to generate strong names.

 

Table 5:  Framework SDK Header Files

 

Enumerating Managed Processes

 

The corpub.h header contains interfaces that allow you to get a list of the managed processes running on your machine and the application domains in those applications. This task involves standard COM programming using COM enumerator interfaces.

// processes.h

#include <objbase.h>

#include <stdio.h>

#include <corpub.h>

#pragma comment(lib, "ole32.lib")

#define NAME_LEN 256

 

void main()

{

   CoInitialize(0);

   HRESULT hr;

   // Get the COR process publisher object.

   ICorPublish* pub;

   hr = CoCreateInstance(__uuidof(CorpubPublish), 0, CLSCTX_INPROC_SERVER, __uuidof(pub), (void**)&pub);

   if (SUCCEEDED(hr))

   {

      // Enumerate the managed processes.

      ICorPublishProcessEnum* pEnum;

      hr = pub->EnumProcesses(COR_PUB_MANAGEDONLY, &pEnum);

      if (SUCCEEDED(hr))

      {

         ICorPublishProcess* processes[5];

         ULONG fetched = 1;

         while(pEnum->Next(5, processes, &fetched) == S_OK && fetched > 0)

         {

            // Get information about each process.

            for (ULONG i = 0; i < fetched; i++)

            {

               WCHAR name[NAME_LEN];

               ULONG32 size = 0;

               // Get the file name.

               processes[i]->GetDisplayName(NAME_LEN, &size, name);

               if (size > 0)

               {

                  wprintf(L"name = %s\n", name);

               }

               // Get the process ID.

               unsigned pid;

               processes[i]->GetProcessID(&pid);

               wprintf(L"\tprocess id = %ld\n", pid);

               // Enumerate the application domains.

               ICorPublishAppDomainEnum* pEnumDomains;

               hr = processes[i]->EnumAppDomains(&pEnumDomains);

               if (SUCCEEDED(hr))

               {

                  ICorPublishAppDomain* appDomains[5];

                  ULONG aFetched = 1;

                  while (aFetched > 0 && pEnumDomains->Next(5,appDomains, &aFetched) == S_OK)

                  {  // Get information about each domain.

                     for (ULONG j = 0; j < aFetched; j++)

                     {

                        WCHAR name[NAME_LEN];

                        ULONG32 size=0;

                        appDomains[j]->GetName(NAME_LEN, &size, name);

                        if (size > 0)

                        {

                           wprintf(L"\t\tname = %s\n", name);

                        } 

                        appDomains[j]->Release();

                     }

                  }

                  pEnumDomains->Release();

               }

               processes[i]->Release();

            }

         }

         pEnum->Release();

      }

      pub->Release();

   }

   CoUninitialize();

}

The CorpubPublish object implements the ICorPublish interface, which you can use to get information about a single managed process or to get access to an enumerator (ICorPublishProcessEnum) to iterate through all the managed processes. Information about a managed process is obtained through ICorPublishProcess, which allows you to get the file name of the process and its Windows process ID. Once you have a process ID, you can pass it to the Win32::OpenProcess function to get a process handle and then get other information about the process using Win32 process functions. The  ICorPublishProcess interface also allows you to enumerate the application domains in a process and from each one get the name and ID of the AppDomain.

 

Getting Information About the Garbage Collector

 

The runtime is represented by an object named the CorRuntimeHost (which is defined in mscoree.h). This object implements the IGCHost interface, which you can call to get information about the garbage collector.

// gc...

ICorRuntimeHost* pHost;

CorBindToRuntimeEx(0, 0, 0, __uuidof(CorRuntimeHost), __uuidof(pHost), (void**)&pHost);

UseTheRuntime(pHost);

IGCHost* pGC;

pHost->QueryInterface(__uuidof(pGC), (void**)&pGC);

COR_GC_STATS stats;

 

memset(&stats, 0, sizeof(stats));

stats.Flags = COR_GC_MEMORYUSAGE│COR_GC_COUNTS;

pGC->GetStats(&stats);

printf("GC called explicitly %ld times\n", stats.ExplicitGCCount);

printf("committed %ld kB\n", stats.CommittedKBytes);

printf("reserved %ld kB\n", stats.ReservedKBytes);

printf("generation 0 has %ld kB\n", stats.Gen0HeapSizeKBytes);

printf("\tcollections: %ld\n", stats.GenCollectionsTaken[0]);

printf("generation 1 has %ld kB\n", stats.Gen1HeapSizeKBytes);

printf("\tcollections: %ld\n", stats.GenCollectionsTaken[1]);

printf("generation 2 has %ld kB\n", stats.Gen2HeapSizeKBytes);

printf("\tcollections: %ld\n", stats.GenCollectionsTaken[2]);

printf("large object heap has %ld kB\n", stats.LargeObjectHeapSizeKBytes);

pGC->Release();

The preferred way to get access to the runtime is through a call to CorBindToRun­time because this API allows you to specify the version of the runtime to load and provides some optimization flags. In this example, I have passed zero for all the options, which indicates that default values will be used. (This is equivalent to calling ::CoCreateInstance to get the run-time object). In this example, we call the user function UseTheRuntime that will call some .NET code and then we dump the garbage collection statistics. IGCHost::GetStats is passed an instance of COR_GC_STATS through which statistics about the garbage collector are returned. This parameter is in/out, and you have to initialize the Flags member to indicate which statistics you require. The IGCHost interface also allows you to configure the garbage collector, and of these methods, perhaps the least dangerous to call is Collect, which allows you to explicitly tell the garbage collector to perform a collection on a specific generation or on all generations.

 

 

Hosting the .NET Runtime

 

The .NET Framework SDK also contains code to allow you to host the .NET runtime. This hosting means that you can create application domains, load types into those domains, and then execute them. Examples of processes that host the .NET runtime are the ASP.NET worker process that is called by IIS to run ASP.NET applications, and Internet Explorer when it is requested to host a .NET control on an HTML page.

Hosting the .NET runtime is only one way to access .NET types from unmanaged code. You can also do the same thing with COM interop, or you can simply compile your unmanaged C++ application as a managed application (with the /clr switch) and import the .NET types with #using. You will decide to host the runtime if you want to have greater control over how application domains are created and the version of the runtime, or if you want to have closer integration and receive events from the runtime.

Hosting the runtime is straightforward, but calling .NET code is not a trivial task because effectively you have to use an equivalent of the .NET Reflection API through COM automation compatible interfaces. We can handle COM interfaces, but when we have to handle the overhead of IDispatch, VARIANT, and SAFEARRAY from C++. If you want to call more than one object or more than one method on an object, it is far better to use another solution. However, if you want to call a single entry point method on an assembly, the pain of calling automation interfaces is worth the effort.

 

Initializing the Runtime

 

The first task to perform is to initialize the runtime. .NET allows side-by-side installation of the runtime; that is, you can have more than one version of the runtime installed on a machine. An application can indicate that it runs under a specific version of the runtime through the <requiredRuntime> element in a configuration file. More than one version of the runtime can execute on a machine at the same time. CorBindToRuntimeEx takes the version of the runtime as a string to its first parameter. This string is in the following format:

v<major>.<minor>.<build>

An example is v1.0.3750. In other words, this string is in a similar format to the naming convention used for the .NET Framework system folder. If you pass a NULL for this parameter, the most recent version of the runtime will be loaded. You can fill a string with the most recent version of the .NET runtime by calling CorGetVersion. The second parameter is called the build flavor and can be wks or svr. This parameter indicates whether you want to load the workstation or server version of the runtime (mscorwks.dll or mscorsvr.dll). If you pass NULL for this parameter, you will get the workstation build. If you have a uniprocessor machine, you will always get the workstation build.

The third parameter of CorBindToRuntimeEx is an optimization flag. For a uniprocessor machine, this flag will allow you to determine whether assemblies are loaded into every application domain, or if they are treated as being domain-neutral. If assemblies are loaded into each application domain and your process has more than one application domain, this strategy can increase the memory footprint of the process. However, if assemblies are domain-neutral, a separate copy of static data must be made for all application domains, and this duplication can slow performance. CorBindToRuntimeEx is passed the CLSID of the run-time object and the interface that you require. Table 6 lists the interfaces that you can request. (These are documented in mscoree.idl.)

 

Interface

Description

ICorConfiguration

Allows you to provide callbacks so that your code is informed when certain thread events occur and when the virtual memory limits have been exceeded

ICorRuntimeHost

Allows you to start or stop the runtime, and to manipulate application domains

IDebuggerInfo

Determines whether a debugger is attached

IGCHost

Gets statistics about and configures the garbage collector

ICorThreadPool

Gets access to the .NET thread pool

IValidator

Validates .NET files

 

Table 6:  Runtime Object Interfaces

 

Typically, you will request the ICorRuntimeHost so that you can start the runtime and get access to an application domain, as shown here:

HRESULT hr;

ICorRuntimeHost* pHost;

hr = CorBindToRuntimeEx(0, 0, 0, __uuidof(CorRuntimeHost), __uuidof(pHost), (void**)&pHost);

if (SUCCEEDED(hr))

{

   pHost->Start();

   RunCodeInAppDomain(pHost);

   pHost->Stop();

   pHost->Release();

}

The user function RunCodeInAppDomain will obtain an application domain and use it to load and execute the user code. There are several methods on ICorRuntimeHost for getting access to an application domain. The first method is GetDefaultDomain, which as the name suggests, is the first domain in the runtime and is created automatically when the runtime starts in the process. If you prefer, you can create your own application domain, and there are two ways to do this: in a single action or in a two-step call. CreateDomain will create a domain with a specific name and return an interface on that domain. CreateDomainSetup will return a pointer to an IAppDomainSetup that you can use to set parameters for the domain and then pass this object to the CreateDomainEx to create the application domain. Finally, you can enumerate all the existing domains by calling methods on the ICorRuntimeHost interface; you do not get a separate enumerator object. The methods that return an application domain actually return an IUnknown interface. There is no application domain interface defined in mscoree.idl. Indeed, the application domain set-up parameters are also passed to CreateDomainEx through an IUnknown pointer. The IAppDomainSetup interface is also notable by its absence in mscoree.idl. The application domain is accessed through a pointer to the _AppDomain interface. These two interfaces are described in the mscorlib.tlb type library, and this is where the fun begins.

The _AppDomain interface is a COM version of the class interface for the System::AppDomain class, so you can use the documentation in the Framework SDK to determine the parameters for the methods. However, the first question is: which method should you call? The problem arises because interfaces in .NET can be overloaded, but in COM they cannot, so when interfaces are exported to COM from .NET, overloaded methods are renamed. For example, there are seven overloads of the AppDomain::Load method, and these overloads appear in the _AppDomain COM interface as methods Load, Load_2, Load_6. You have to use OLEView to look at the signatures of these methods to determine which method you intend to call. You get a description in the type library for _AppDomain because this class is marked with the [ClassInterface] attribute to indicate that it is ClassInterfaceType::AutoDual. However, this behavior is not the default, and you are discouraged from using this attribute value on your own classes. The default is not to provide a definition for a class interface for COM and only to support late binding. Take, for example, the _Module interface (the class interface of the System::Reflection::Module class). The type library gives this:

[odl, uuid(D002E9BA-D9E3-3749-B1D3-D565A08B13E7), hidden, oleautomation]

interface _Module : IDispatch {};

There is no indication of the methods implemented on this interface. “But,” you say, “you always have the option of using the documentation for the Module class.” Yes, you do, but what about overloaded methods? To be absolutely sure, you have to write code to access the type information that is generated dynamically for the Module object (IDispatch::GetTypeInfo) and check for a method that has the same parameter types and with a name the same as you expect, or with an underscore and a number. This process is all rather messy. To make even trivial calls to .NET Framework library classes requires lots of C++ simply to make the automation calls. This is why said earlier that if you want to call more than an entry point function, you should consider some other method of accessing the runtime from unmanaged code. IJW, of course, is your perfect C++ solution.

 

Summary

 

The more .NET code you write, the more you realize that there is more to the .NET Framework than scripting together Web controls on an ASP.NET page or controls on a form. The .NET Framework has been built from the bottom up to be secure, flexible, and fully configurable. In this module, we have given details of how assemblies are implemented in PE files and how metadata is stored in those files and accessed through the unmanaged API. We have also shown how applications are configured and some of the great things you can do, as well as some may be considered the deficiencies in the current design.

 

Part 1 | Part 2 | Part 3 | Part 4 | Part 5 | Part 6 | Part 7 | Part 8

 


 

< C++ .Net System Programming 7 | Main | Old To New C++ .Net 1 >