< C++ .Net System Prog. 2 | Main | C++ .Net System Prog. 4 >


 

Early Stages of the C++ .Net 17

(Managed Extensions for C++)

 

 

The following are the topics available in this page.

  1. Assembly Format

  2. Version

  3. Culture

  4. Assembly Strong Name

  5. Assembly Configuration

  6. Configuration Files

  7. Application Settings

 

 

 

Assembly Format

 

An assembly contains a manifest, which is essentially a repository for information about the files that constitute the assembly, the resources it holds, the security permissions it requires, and the assemblies that the current assembly is statically linked to. The manifest is contained in a PE file, either a DLL or an EXE. This DLL is the file that you specify in a #using statement.

An assembly is made up from one or more modules. A module contains code and is a mechanism for the .NET runtime to load only the code that is actually being executed. However, it is worth pointing out that you will rarely want to create an assembly with more than one module. Indeed, the .NET Framework assemblies are all single-module assemblies. There are two main situations in which you will want to have more than one module. The first case is when you have a library with types that you use frequently and types that you will rarely use. In this case, you can put the rarely used types in a separate module. When the assembly is loaded, the module with the frequently used types will be loaded, and the other module will be loaded into memory only in the rare situation in which the types it defines are referenced.

 The other situation in which you will use multiple modules is when the assembly is designed to be downloaded from another machine. You can put types that are likely to need updates in a separate module, and when you update a module, only this module will be downloaded. However, as you will find out in the section “Verifiable Code,” the C++ compiler cannot be used to create assemblies that are intended to be downloaded because the .NET loader will refuse to load such an assembly sourced from another machine. Each module will have metadata containing information about the types that the module implements and the assemblies that it references. One module will contain the manifest. In addition to information about the types that this module implements and the assemblies it references, the manifest contains metadata about the other modules in the assembly and information about the assembly. You can view the manifest with ILDASM, but note that this tool identifies the tables of type definitions and assembly references as MANIFEST even if the module does not contain an assembly manifest.

Two important pieces of information in an assembly manifest are the security permissions required by the assembly and the complete name of the assembly. Because an assembly can be made up of more than one file, there must be some mechanism to name the entire assembly. When you build an assembly, you will get one or more PE files. The PE file that contains the manifest supplies the short name of the assembly; all assemblies have a short name. In addition, an assembly can have metadata to indicate the version, the culture, and a public key of the publisher of the assembly. The full name of an assembly is a combination of these four: the short name, version, culture, and public key. The version, culture, and public key for an assembly are provided through custom attributes.

Version

 

The version of an assembly is supplied through the [AssemblyVersion] attribute. The version is supplied as the parameter to this attribute as a string in this format:

<major>.<minor>.<build>.<revision>

Each part of the version string is a number, and each number is separated by a period. There is a major and a minor version, a build number, and a revision.

 You must provide the major version, and the rest can be regarded as optional and will be assumed to be zero if you do not specify them. If you provide an asterisk for the build, the compiler will generate the build number by calculating the number of days since the year 2000 from the build date, and then the compiler will generate the revision number by calculating the number of seconds from midnight module 2 from the build time. This mechanism means that each time an assembly is compiled, the build and revision are changed. If the assembly has a strong name, the runtime will create the complete name of the referenced assembly using the version stored in the referring assembly. If the runtime cannot find an assembly with this exact name, you can change this behavior with a configuration file. See the section “Locating Assemblies” later in this module you will get a FileLoadException. The problem with using * within [AssemblyVersion] is that if the assembly is a library assembly and has a strong name, you have to compile the assemblies that use the library every time you recompile the library. Of course, you might decide to recompile the library because the public interface of the types exported from the assembly (that is, the public members of public types) has changed, so the users of the library must be recompiled to take advantage of the new public interface. If only the implementation of those types has changed, it should be unnecessary to recompile the users of the library.

Furthermore, when you use the [AssemblyVersion] attribute in a C++ file, the compiler will change the .ver metadata attribute, but it does not change the VERSIONINFO unmanaged resource in the final assembly. Because you have two versions to keep synchronized, it makes no sense to rely on the compiler to provide the values for the build and revision. For all of these reasons, It is recommended that you do not use * in the version string for the [AssemblyVersion] attribute.

If you have multiple source files for your project, you should use [AssemblyVersion] in only one file; if you have this attribute in more than one source file, the linker will notice this duplication, issue a warning, and use the version in the .obj file that was last passed to the linker.

The .NET Framework also provides an attribute named [AssemblyFile­Version], but this attribute has no effect on the assembly version. The compiler will add metadata for the attribute as a custom attribute. The compiler does read the attribute, and it will validate the value passed to the attribute to ensure that this string contains only numbers and periods. The parameter to [AssemblyFileVersion] is a string in the form major.minor.build.revision, but you can omit parts of the string except for major. Similar to the assembly version, the value passed in [AssemblyFileVersion] is not automatically used to update the VERSIONINFO unmanaged resource. You will have to manually synchronize the managed and unmanaged versions. This can be done with a script that obtains the relevant files from Visual SourceSafe, changes the versions, and then checks the files back in. At each milestone in the development cycle, we can run the script for all the assemblies in the project and set the managed and unmanaged version to the same value.

 

Culture

 

The culture for an assembly is especially important for a library assembly that is used as a satellite assembly. The culture is also useful for library assemblies in general, but it is of no use for an EXE assembly because an EXE assembly should be culture neutral, and if it needs a localized resource, an EXE should use a satellite assembly. For this reason, the C++ compiler will issue an error when you attempt to add a culture to an EXE assembly. You add a culture to a library assembly through the [AssemblyCulture] attribute and pass the culture identifier to the constructor.

 

Assembly Strong Name

 

You give an assembly a strong name by providing a public/private key pair. When the assembly is built, the compiler will read the files that constitute the assembly and generate a hash for each one. This hash is added to the manifest of the assembly so that the .NET loader can check to see whether the file has changed when it is loaded. The default hash algorithm is SHA-1, but you can change the algorithm with the [AssemblyAlgorithmId] attribute. The options are given by the AssemblyHashAlgorithm enumeration, either SHA-1 or MD5.

Once the compiler has created the manifest, the compiler will create a hash (always using SHA-1) from the entire PE file that contains the manifest, and it will sign this hash with the private key that you provide. The hash and the public key are stored in the assembly (in a location that is not hashed) so that when an assembly is loaded, the loader can generate a hash and compare this hash with the signed hash in the assembly. If the two do not agree, the file has been tampered with and the loader will not load it. Note that the signing occurs on the file with the manifest; if an assembly has other modules, the hash of the module is not signed. To create a public/private key pair, you run the sn.exe tool. This tool can create a key pair in a file, or it can put the key pair in a cryptographic container. Typically, you will only ever want to run this utility once to create the publisher key pair for your company. You can then use the same key pair for every assembly that you produce that has a strong name. Because you will only ever need one key pair, it makes sense to install this key pair into a cryptographic container so that the key pair will be available from any folder on your machine. It is a two-step process to put a key pair in a container. The first step is to generate a key pair in a file, as shown here:

sn –k RTG.snk

The file RTG.snk will contain the key pair. This file can then be installed in a key container, a part of the key database that contains all the key pairs (exchange and signature key pairs) belonging to a specific user with the following:

sn -i RTG.snk RTG

The key pair in the file will be installed into the container named RTG. Each cryptographic service provider on your machine has a key database that is used to store persistent keys. There is a machine-wide database and a user-specific database. You can use the -m switch on sn to specify which database to use and you can use the -m switch to check the current setting. (The machine key database is stored in the \Documents and Settings\All Users\Application Data\Microsoft\Crypto\RSA\MachineKeys folder, and the per-user database is stored in \Documents and Settings\<User>\Application Data\Microsoft\Crypto\ RSA\<SID>, where <User> is the user’s name and <SID> is the user’s SID.)

To apply a strong name to an assembly, you use the [AssemblyKeyFile] or [AssemblyKeyName] attribute. The former is passed the name of the file that holds the key; the latter is passed the name of the crypto key container. You can view the public key that is added to the assembly using ILDASM, and you’ll find that there is a metadata entry named .publickey that lists the key. It is interesting that the C++ compiler also adds a custom attribute for both of these even though this is unnecessary.

The public key is typically 600 bytes or so and is a key component of the strong name. However, if the name of an assembly contained the entire public key, the name would be extremely long, so instead of using the public key in the strong name, .NET uses a 64-bit hash of the public key named a public key token. The .NET Framework SDK exports functions from mscoree.dll to generate key pairs and public key tokens. These functions are prototyped in strongname.h, as shown in this example:

// Create a key pair.

LPBYTE key;

ULONG sizeKey;

StrongNameKeyGen(NULL, 0, &key, &sizeKey);

// Do something with the key pair.

 

// Create a token from the key pair.

LPBYTE token;

ULONG sizeToken;

StrongNameTokenFromPublicKey(key, sizeKey, &token, &sizeToken);

 

// We know that the token is 64 bits.

wprintf(L"\n%02x%02x%02x%02x%02x%02x%02x%02x\n",

   token[0], token[1], token[2], token[3],

   token[4], token[5], token[6], token[7]);

 

// Free the buffers used by the API.

StrongNameFreeBuffer(token);

StrongNameFreeBuffer(key);

The strong name APIs return buffers allocated by the API; you must free these buffers by calling StrongNameFreeBuffer. There are functions in strongname.h to install key pairs in a key container, to extract a public key from an assembly, to perform tasks, such as hash blobs of data and files, and to verify a signed manifest file. Clearly, the sn tool is implemented using these functions (and you can verify this by running the DEPENDS utility on mscoree.dll). The following Figures show the DEPENDS example against mscoree.dll.

 

Depends executable command line tools

 

Figure 11

 

Running the depends tool

 

Figure 12

 

Assembly Configuration

 

One of the goals of .NET is to simplify application deployment. Microsoft calls this process XCOPY deployment because the implication is that you can simply copy the application (using Windows Explorer, or indeed, the command-line XCOPY command) to copy the application files to the target machine. Related to XCOPY deployment is DEL un-installation. The idea is that when you want to remove an application from your system, you merely delete the files. Of course, not all applications can be installed and uninstalled in this way (applications that are Windows Services are an example), but the situation is certainly better than in the days before .NET. One area where Microsoft has made improvements to facilitate XCOPY deployment is in configuration. In the early days of Windows, the preferred configuration technique was to use INI files: an application could store its settings in a global INI file named win.ini or in a private file. The API you use to read INI files is dated, and the format of these files is rather restrictive. Because these settings were file-based, while one application was writing to the INI file, the application had a lock on the file, so other applications could not have access.

To get around these problems, newer versions of Windows provide a hierarchical database named the registry. The actual underlying technology of this database is hidden from you through the API. However, access to the registry is more sophisticated than mere file access; it is a multi-user system where two threads can access different parts of the registry at the same time. Registry keys can have access control lists, so you can control who has access to a key. The API also has a simple mechanism to have global settings for all users (HKEY_LOCAL_MACHINE, commonly abbreviated HKLM) and to allow you to have settings specific to the current user (HKEY_CURRENT_USER, or HKCU). (If you have an account with sufficient privileges, you can even access the registry on another machine, which means that administrators have less distance to walk when administering machines.)

The problem with the registry comes in two forms: configuration and bloat. When an application is installed on a machine, the installation program must add values for the application into the registry to allow the application to run. Unless you have privileged knowledge about the values that the application needs, you have no choice but to use the installer program, which means that XCOPY deployment is not possible. When you remove an application, you also have to remove the registry entries, which bring us to bloat. Even if your applications do uninstall themselves properly, the registry is a hungry beast and will grow with time until its size reaches a user-specified setting, at which point you will get a dire warning from the system telling you to increase the registry size. One situation where bloat is an issue is with COM object registration. The bloat associated with COM object registration is such an issue for the developer that we have gotten into the habit of adding a special value to the main key of my COM objects when built for DEBUG. We can then clean the registry at any time by running a utility that looks for the special value and then deletes the key and its subkeys.

The .NET Framework introduces a new configuration system. XML is ubiquitous in the .NET Framework, and it will come as no surprise to learn that the .NET Framework uses XML files to hold configuration information. It is interesting that Microsoft appears to have performed a U-turn with configuration files and has gone back to the old days of providing a user-readable file for each application, rather than a central repository. In this section, we will describe how configuration files are used and explain some nice configuration file features, but bear in mind that configuration files do not solve the issue of multiple threads accessing a single file locked by another thread writing to the file. The .NET configuration file API essentially ignores this issue by treating the files as read-only by applications. Furthermore, there are no per-user configuration files, so if you have an application that can be used by several users on the same machine, you will have to find another mechanism to save per-user settings.

 

 

Configuration Files

 

Each configuration file is an XML file and has the extension .config. The root element in a configuration file is named <configuration>; the other elements in a configuration file are defined by the configuration file schema and can be extended. Your machine will have configuration files for security and for applications. For applications, there will be a single, centralized file named machine.config that has configuration settings used by all applications. In addition, each application can have a configuration file that has a name in the form <app>.config, where <app> is the EXE file for the application, so an application named MyApp.exe will have a configuration file named MyApp.exe.config. You can also have files to configure .NET remoting, but these files are loaded in a different way than application configuration files.

When you read a configuration section, you will get an amalgamation of the settings in the application file and the machine’s configuration file. If there is a setting with the same name in both files, the application file setting takes precedence. Under the covers, when a configuration section is requested and it is found to exist in the application configuration file or the machine.config file, the setting is cached in a Hashtable. This mechanism reinforces the statement that made earlier: configuration files are read-only because changes that are made while the application is running are not guaranteed to be readable by the application. Note that configuration files are for applications; you cannot have a configuration file for a library assembly. ASP.NET uses library assemblies, so to get around this restriction, ASP.NET applications have configuration files named web.config in the Web application’s folder.

When you develop an application, you will find the application configuration file useful because there are settings that you can make to configure debugging options. Configuration files are also useful for deployed applications. There are two main scenarios: user configuration settings and run-time settings. User configurations settings are whatever you choose to use, and typically you will put these in the <appSettings> section of the file. Configuration files also have settings that are read by the runtime. You do not have to do anything to get the runtime to read the settings; the runtime will automatically load your configuration file and look for the values that it requires. By default, Visual Studio .NET does not allow you to manage configuration files for C++ projects (although it does for C# projects). Common configuration file sections that you can have in an application configuration file are given in Table 2. Note that the capitalization used in the section name is important; if you use a different capitalization, the system will throw a configuration exception.

 

Section

Description

<appSettings>

Custom configuration settings

<configSections>

Allows you to extend configuration files

<runtime>

Information about garbage collection, assembly binding, and probing

<startup>

Information about the version of the runtime that the application requires

<system.diagnostics>

Settings for tracing applications

<system.net>

Configuration settings for the System.Net classes that allow you to use sockets

<system.runtime.remoting>

Configuration settings for remote objects and remoting channels

<system.web>

Configuration settings for ASP.NET applications

 

Table 2:   Common Configuration File Sections

 

Application Settings

 

You can supply application settings in a configuration file through the <app­Settings> section. Remember, the application regards these sections as read-only. The user of an application provides the application settings. Essentially, this section can be regarded as equivalent to command-line switches. Here is an example configuration file:

<!-- MyForm.exe.config -->

<configuration>

   <appSettings>

      <add key="BackColor" value="RED"/>

      <add key="Height" value="100"/>

      <add key="Width" value="200"/>

   </appSettings>

</configuration>

It is important to point out that these pairs are key-value pairs and not name-value pairs. Just about every exception we have ever received from using the <appSettings> section has been because we have used name instead of key. (The confusion occurs because other sections in configuration files are name-value pairs.) The few exceptions that we have had that have not been caused through using name have been caused because we have used the wrong capitalization in the section names.

There are several ways to read these settings. The simplest way is to use a class named AppSettingsReader in the System::Configuration namespace. This class has a single method named GetValue that is provided the name of the setting and the type. The method will read the setting and attempt to convert the value from a string to the type you specify by calling the Parse method on the target type. In this example, we want to specify a color but the Color class does not have a Parse method, so we specify that the setting is read as a string and we do the conversion ourselves:

// myform.cpp

Form* frm = new Form;

AppSettingsReader* reader = new AppSettingsReader;

String* strColor = static_cast<String*>(reader->GetValue(S"BackColor", __typeof(String)));

frm->BackColor = Color::FromName(strColor);

Int32 __box* width = static_cast<Int32 __box*>(reader->GetValue(S"Width", __typeof(Int32)));

frm->Width = *width;

Int32 __box* height = static_cast<Int32 __box*>(reader->GetValue(S"Height", __typeof(Int32)));

frm->Height = *height;

Application::Run(frm);

This code reads the BackColor setting as a string and then uses the FromName method to create a color from a known color name. This color value is then used for the background color of the form. The code also reads the Width and Height settings, but because Int32 does have a Parse method, the code can allow GetValue to do the conversion. The other way to read values in the <appSettings> section is to read the entire section in one go and access these settings through a collection, and to perform this task, you use the Configuration::AppSettings static property. The <appSettings> section, like many sections in a configuration file, has nested sections, and to help you read these, the .NET Framework defines collection classes.

// myform2.cpp

Form* frm = new Form;

AppSettingsReader* reader = new AppSettingsReader;

String* strColor = ConfigurationSettings::AppSettings->Item[S"BackColor"];

frm->BackColor = Color::FromName(strColor);

String* strWidth = ConfigurationSettings::AppSettings->Item[S"Width"];

frm->Width = Int32::Parse(strWidth);

String* strHeight = ConfigurationSettings::AppSettings->Item[S"Height"];

frm->Height = Int32::Parse(strHeight);

Application::Run(frm);

Configuration::AppSettings is a NameValueCollection, which has an indexer property. You pass the name of the setting as the parameter to the Item property, and it will return back a string. Even if the setting is for a type that has a Parse method (as in this case with Width and Height), you still have to explicitly convert from a string. You can also have an <appSettings> section in the machine.config file that will hold global settings used by all applications. At run time, you will get a combination of the settings from machine.config and from your own application configuration file. This arrangement means that your application might inherit settings from machine.config that the user does not want the application to receive. To remove an individual item, you can use the <remove> tag in the application configuration file; to remove all the settings inherited from the machine.config file, you can use the <clear> element. If you have many items in your configuration file, you might decide it would be better to split the file into two files. You can do this operation with the <appSettings> section by providing a file attribute to the tag:

<configuration>

   <appSettings file="otherSettings.xml">

      <add key="BackColor" value="RED"/>

   </appSettings>

</configuration>

The system reads the application settings in the application configuration file first, followed by the settings in the file you specify. If any settings are replicated, the values given in the file specified by the file attribute will take precedence. For this example, the extra file would look like this:

<!-- otherSettings.xml -->

<appSettings>

   <add key="BackColor" value="GREEN"/>

   <add key="Height" value="100"/>

   <add key="Width" value="200"/>

</appSettings>

Notice that the root of this file is <appSettings>. The application will create a green form because the value in this file will be used instead of the value provided by the application configuration file. Although this facility is of some use, we can see that it could have been even more useful. For example, if we could give the name of an environment variable for the file, we would have a very simple mechanism to provide per-user settings through the %USERNAME% variable. Sadly, the mechanism does not work this way.

 

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

 


 

< C++ .Net System Prog. 2 | Main | C++ .Net System Prog. 4 >