Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro Visual C++-CLI And The .NET 2.0 Platform (2006) [eng]-1

.pdf
Скачиваний:
70
Добавлен:
16.08.2013
Размер:
24.18 Mб
Скачать

748 C H A P T E R 1 8 A S S E M B L Y P R O G R A M M I N G

give you a heads-up.) The reason you can be ignorant is because adding an assembly to the GAC requires you to simply drag it from your development directory and drop it on the Windows Explorer assembly directory. If you want to perform this process in a batch routine, you can use a utility called gacutil.exe to install and uninstall your assembly. To install your assembly, use

>gacutil /i <assembly name>.dll

To uninstall the assembly, use

>gacutil /u <assembly name>.dll, Version=<version number>

It is even easier to install assemblies using a setup project because the copying to the GAC is handled for you.

The Shared Assembly’s Strong Name

There is a catch to global assemblies. They require that they be signed by what is called a strong name. A strong name provides three necessary enhancements to assemblies:

It makes the name of the assembly globally unique.

It makes it so that no one else can steal and use the name (generally known as spoofing).

It provides a means to verify that an assembly has not been tampered with.

The strong name provides these enhancements by adding three things to the assembly: a simple text name, a public key, and a digital signature. The combination of the simple text name and the public key guarantees the name is globally unique, as the public key is unique to the party creating the assembly, and it is assumed that the party will make the simple text assembly name unique within their own development environment.

The combination of the public key and the digital signature verifies that no spoofing or tampering occurred. It does this by adding public/private key encryption to the assembly.

Note Public/private key encryption uses two keys as its name suggests. The private key is used to encrypt something, and the public key is used to decrypt it. What makes this combination secure is that only a corresponding public key can be used to decrypt something encrypted by the private key.

So how does public/private key encryption apply to global assemblies? Before you get all excited, you should know that an assembly is not encrypted. Instead, at compile time the compiler creates a hash signature based on the contents of the assembly and then uses the private key (of public/private encryption) to encrypt the hash signature into a digital signature. Finally, the digital signature is added to the assembly. Later, when the assembly is loaded by the CLR, the digital signature is decrypted using the public key back into the hash signature, and the hash signature is verified to make sure that the assembly is unchanged.

The reason this all works is that only the owner of the private key can create a valid digital signature that can be decrypted by the public key.

Like most things in .NET application development, what actually happens is a lot more complex than what you need to do to get it the happen. In this case, to add a strong name to an assembly requires two very simple steps. First, you create a strong name key file by typing the following statement at the command prompt:

> sn -k StrongNameFileName.snk

C H A P T E R 1 8 A S S E M B L Y P R O G R A M M I N G

749

Then you update [AssemblyKeyFileAttribute] in the AssemblyInfo.cpp file, which incidentally is created by all C++/CLI templates:

[assembly:AssemblyKeyFileAttribute("StrongNameFileName.snk")];

You can place the key in the project directory as the preceding example shows, or you can place it anywhere on your computer and provide a full path to the attribute.

Re-signing an Assembly

If you are security conscious, you may have seen a big problem in the preceding strong name system. If you are developing software in a team environment, everyone who needs to update the assembly must have access to the private key so that the assembly can be accessed using the same public key. This means there are a lot of potential areas for security leaks.

To remedy this, the strong name utility sn.exe has an additional option. It provides the capability for an assembly to be re-signed. This allows privileged developers a chance to sign the assembly with the company’s private key before releasing it to the public. The command you need to type at the command line is

> sn -R <assembly name> <strong key file name>

Notice this time instead of the –k option you use the –R option, stating you want to replace the key instead of create one. You also provide the utility a completed assembly and a previously created strong key file.

Signcoded Digital Signature

Nowhere in the preceding strong name process is the user of the assembly guaranteed that the creator of the strong key is a trusted source, only that it is unchanged from the time it was created.

To remedy this, you need to execute the signcode.exe wizard on your assembly to add an authentic digital certificate created by a third party. Once you have done this, the user of the assembly can find out who created the assembly and decide whether he or she wants to trust it.

Caution You need to compile the assembly with the “final” strong name before you signcode it. The signcode.exe wizard only works with strong named assemblies. Also, re-signing a signcoded assembly invalidates its authentic digital certificate.

Versioning

Anyone who has worked with Windows for any length of time will probably be hit at least once with DLL Hell, the reason being that versioning was not very well supported in previous Windows developing environments. It was possible to swap different versions of .dlls in and out of the registry, which caused all sorts of compatibility issues. Well, with .NET this is no longer the case, as versioning is well supported.

That being said, a word of caution: The CLR ignores versioning in private assemblies. If you include a private assembly in your application’s directory structure, the CLR assumes you know what you are doing and will use that version, even if the correct version, based on version number, is in the GAC.

The .NET Framework supports a four-part version: major, minor, build, and revision. You will most frequently see version numbers written out like this: 1.2.3.4. On occasion, however, you will see them like this: 1:2:3:4. By convention, a change in the major and minor numbers means that an

750 C H A P T E R 1 8 A S S E M B L Y P R O G R A M M I N G

incompatibility has been introduced, whereas a change in the build and revision numbers means compatibility has been retained. How you actually use version numbers, on the other hand, is up to you. Here is how the .NET Framework handles versioning in a nutshell: Only the global assembly

version that was referenced at compile time will work in the application. That is, all four version parts need to match. (Well, that is not quite true. You will see a way to overrule which version number to use later in this chapter.) This should not cause a problem even if there is more than one version of a shared assembly available, because multiple versions of a shared assembly can be placed without conflict into the GAC (see Figure 18-5). Okay, there might be a problem if the shared assembly with the corresponding version number is not in the GAC, as this throws a

System::IO::FileNotFoundException exception.

Figure 18-5. Multiple versions of an assembly in the GAC

Setting the Version Number

Version numbers are stored as metadata within the assembly, and to set the version number requires that you update the AssemblyVersionAttribute attribute. To make things easier for you, the Visual Studio 2005 project template wizard automatically provides a default AssemblyVersionAttribute attribute within the AssemblyInfo.cpp file.

You set the version number by simply changing the dummy value

[assembly:AssemblyVersionAttribute("1.0.*")];

to a value that makes sense in your development environment, for example:

[assembly:AssemblyVersionAttribute("3.1.2.45")];

Notice the asterisk (*) in the default version number value provided by Visual Studio 2005. This asterisk signifies that the compiler will automatically create the build and revision numbers for you. When the compiler does this, it places the number of days since January 1, 2000, in the build and the number of seconds since midnight divided by two in the revision.

Personally, I think it’s a mistake to use the auto-generated method, as the version numbers then provide no meaning. Plus, using auto-generated numbers forces you to recompile the application referencing the assembly every time you recompile the shared assembly. Auto-generated numbers aren’t so bad if the application and the shared reference share into the same solution, but they aren’t so good if the application and the shared reference share into different solutions, and even worse if different developers are developing the application and shared assembly.

C H A P T E R 1 8 A S S E M B L Y P R O G R A M M I N G

751

Getting the Version Number

It took me a while to figure out how to get the version number out of the assembly (but that might just be me). As I found out, though, it’s really easy to do, because it’s just a property of the name of the assembly. I think the code is easier to understand than the explanation:

Assembly ^assembly = Assembly::GetExecutingAssembly();

Version ^version = assembly->GetName()->Version;

The only tricky part is getting the currently executing assembly, which isn’t too tricky because the .NET Framework provides you with a static member to retrieve it for you.

No DLL Hell Example

Now that you’ve covered everything you need to create a shared assembly, you’ll create one. Listing 18-7 shows the source code of a very simple class library assembly containing one class and one property. The property contains the version of the assembly.

Listing 18-7. A Shared Assembly That Knows Its Version

using namespace System;

using namespace System::Reflection;

namespace SharedAssembly

{

public ref class SharedClass

{

public:

property System::Version^ Version()

{

System::Version^ get()

{

Assembly ^assembly = Assembly::GetExecutingAssembly(); return assembly->GetName()->Version;

}

}

};

}

The code is short, sweet, and offers no surprises. Listing 18-8 contains a filled-in AssemblyInfo.cpp file. To save space, all the comments have been removed.

Listing 18-8. A Standard AssemblyInfo.cpp File

using namespace System::Reflection;

using namespace System::Runtime::CompilerServices;

[assembly:AssemblyTitleAttribute("A Shared Assembly")]; [assembly:AssemblyDescriptionAttribute("An assembly that knows its version")]; [assembly:AssemblyConfigurationAttribute("Release Version")]; [assembly:AssemblyCompanyAttribute("ProCppCLI")]; [assembly:AssemblyProductAttribute("Pro C++/CLI Series")]; [assembly:AssemblyCopyrightAttribute("Copyright (C) by Stephen Fraser 2005")]; [assembly:AssemblyTrademarkAttribute("ProCppCLI is a Trademark of blah")]; [assembly:AssemblyCultureAttribute("")];

752 C H A P T E R 1 8 A S S E M B L Y P R O G R A M M I N G

[assembly:AssemblyVersionAttribute("1.0.0.0")];

[assembly:AssemblyDelaySignAttribute(false)];

[assembly:AssemblyKeyFileAttribute("SharedAssembly.snk")];

[assembly:AssemblyKeyNameAttribute("")];

You saw most of the important code earlier in this chapter, so I won’t go over this in detail. I also think that most of the rest of the code is self-explanatory. Only the AssemblyCultureAttribute attribute needs to be explained, and I do that a little later in this chapter.

Of all the attributes in the preceding source file, only two attributes need to be filled in to enable an assembly to be a shared one. The first attribute is AssemblyVersionAttribute. It already has a default value but I changed it to give it more meaning to me.

The second attribute is AssemblyKeyFileAttribute, in which you place the strong key. Remember, you can either pass a full path to the attribute or use a key in the project source directory. Because I’m using a strong key file in the project source, I have to copy my key file SharedAssembly.snk into the project’s source directory.

Before you compile the project, change the project’s output directory to be local to the project and not the solution. In other words, change the project’s configuration properties’ output directory to read only $(ConfigurationName) and not the default $(SolutionDir)$(ConfigurationName). The reason you want to do this is that you don’t want a copy of SharedAssembly.dll in the same directory as the application assembly referencing it, because otherwise it will be used instead of the copy in the GAC.

Now, when you compile the project, an assembly called SharedAssembly.dll is generated in the project’s Debug or Release directory, depending on which environment you’re doing the build in. This file needs to be copied to the GAC either by dragging and dropping it there or via gacutil.exe. Figure 18-6 shows what the entry in the Windows Explorer GAC display looks like.

Figure 18-6. SharedAssembly in the GAC

Now you’ll create an application assembly to reference the shared assembly (see Listing 18-9). All this application does is write out the version number of the shared assembly.

Listing 18-9. Referencing a Shared Assembly

using namespace System;

using namespace SharedAssembly;

void main()

{

SharedClass ^sa = gcnew SharedClass(); Console::WriteLine(sa->Version);

}

The code is not new, but to get this to work you need to reference the assembly SharedAssembly.dll. It is important to understand that the assembly you reference during the compile does not need to be the same as the one that you actually execute at runtime. They just have to have the same name,

C H A P T E R 1 8 A S S E M B L Y P R O G R A M M I N G

753

version, and public key token. Therefore, even though you are going to use the assembly within the GAC, you reference the assembly within the solution to get the definition of the SharedClass class and the Version property.

To reference SharedAssembly.dll, you need to perform the following steps:

1.Open the Properties window.

2.Select the References folder.

3.Click the Add New Reference button. This will bring up the Add Reference dialog box.

4.Select the Projects tab.

5.Select the shared assembly from the list.

Or, if the shared assembly is in a different solution, click Browse, navigate to the location of the assembly, and then select the assembly.

6.Click OK.

7.In Build Properties, set Local copy to False (see Figure 18-7).

Figure 18-7. The Add Reference dialog box

The most important step of the preceding sequence is step 7. This step causes the build process not to make a local copy of the assembly and instead causes the GAC to be used as the source of the assembly.

Caution Don’t miss step 7. If you do, then you are not using a shared assembly, just a local copy of the assembly that gets moved to the application’s root directory during the compile process.

754 C H A P T E R 1 8 A S S E M B L Y P R O G R A M M I N G

Run ReferenceSharedAssembly.exe. You should get something similar to what is shown in Figure 18-8.

Figure 18-8. The result of executing ReferenceSharedAssembly

Now let’s see what happens if you change your shared assembly and give it a new version number like this:

[assembly:AssemblyVersionAttribute("1.1.0.0")];

Recompile only the SharedAssembly project and then move the new assembly SharedAssembly.dll to the GAC. First off, notice that now there are two SharedAssembly entries in the GAC that differ by version number.

Run ReferenceSharedAssembly.exe again. (Important: Do not recompile when asked.) Nothing has changed, has it? You still get the same output. This is versioning in action. Why do you get the original version of the shared assembly? Because when you compiled the application program, you tightly bound it to version 1.0.0.0 of the shared assembly. Thus, when it executes, it can only load version 1.0.0.0.

Just for grins and giggle, delete version 1.0.0.0 from the GAC and run ReferenceSharedAssembly.exe a third time. Nice abort don’t you think? The reason the program aborts is because even though there is a copy of SharedAssembly in the GAC, it is the wrong version (1.1.0.0). ReferenceSharedAssembly.exe is tightly bound to version 1.0.0.0.

Tip If you are like me and have your compile environment automatically compile all changed modules before executing, then the easiest way to test this is to compile only SharedAssembly and then go to the command line and run ReferenceSharedAssembly.exe from there.

Application Configuration Files

An alarm might be going off in your head right now. Does this mean that whenever you change a shared assembly, you have to keep the same version number or you have to recompile every application that uses shared assembly so that it can be accessed? How do you release a fix to a shared assembly?

The .NET Framework provides a solution to this problem by adding a configuration file to the application that specifies which assembly you want to load instead of the bound version. The application configuration file has the same name as the executable plus a suffix of .config. Therefore, for the preceding example, the application configuration file would be called ReferenceSharedAssembly.exe.config. Yes, the .exe is still in the name.

The application configuration file will look something like Listing 18-10.

C H A P T E R 1 8 A S S E M B L Y P R O G R A M M I N G

755

Listing 18-10. An Application Configuration File

<configuration>

<runtime>

<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly>

<assemblyIdentity name="SharedAssembly" publicKeyToken="332a33ed1547b4e6" />

<bindingRedirect oldVersion="1.0.0.0" newVersion="1.1.0.0" />

</dependentAssembly>

</assemblyBinding>

</runtime>

</configuration>

The only two elements you have to worry about in the file are <assemblyIdentity> and <bindingRedirect>. <assemblyIdentity> contains the identity of the shared assembly that you want to use a different version with. Notice that all the information you need to identify the shared assembly can be found in the Windows Explorer GAC view.

Next is the key to assigning a different version to the <bindingRedirect> element. This element specifies the old version, or the version that the application assembly currently references, and then the new version that you want it to access instead. A cool feature is that the oldVersion tag can take a range:

<bindingRedirect oldVersion="1.0-1.1" newVersion="1.1.0.0" />

Now that you have the file created, place it in the same directory as the executable and run ReferenceSharedAssembly.exe again. (Important: Do not recompile when asked.) This time you will get the output shown in Figure 18-9.

Figure 18-9. The result of executing ReferenceSharedAssembly with an application configuration file

As a final note to application configuration files, you can also set the newVersion tag to a prior version of the assembly:

<bindingRedirect oldVersion="1.1.0.0" newVersion="1.0.0.0" />

This comes in handy when the new version is found to not be compatible and you need to fall back to a previous version.

Resources

When you finally get to the point of running your software, usually there are other things needed for it to run besides the executable. For example, you might find that you need images, icons, cursors, or, if you are going to globalize the application, a culture’s set of strings. You could fill your application directory full of a bunch of files containing these “resources.” But if you did, you would run the

756 C H A P T E R 1 8 A S S E M B L Y P R O G R A M M I N G

risk of forgetting something when you deployed your application. I think a better solution is to group common resources into .resources files. Then, optionally, embed the .resources files into the assembly that uses the contents of the .resources files. My thought is that with fewer files floating around, fewer things can get lost.

You have three ways to work with grouped resources in the .NET Framework:

You can place the grouped resources in .resources files and then work with them as separate entities. This allows you to switch and swap the .resources files as needed. It also allows you to work with the resources within the .resources files in a dynamic fashion.

You can embed the resources directly into the assembly that uses them. This method has the least flexibility, but you can be secure in the knowledge that everything you need to run the assembly is available.

You can combine the two previous methods and create what the .NET Framework calls satellite assemblies. These are assemblies containing only resources, but at the same time, they directly link to the assembly that uses the resources within them. You will see this use of resources when you look at globalization and localization later in this chapter.

Creating Resources

The .NET Framework provides you with two text formats for creating .resources files: a text file made up of name/value pairs and an XML-formatted file called a .resx file. Of the two, the name/value-formatted file is much easier to use, but it has the drawback of supporting only string resources. On the other hand,

.resx files support almost any kind of resource, but unfortunately they are extremely hard to hand code. Most likely, because .resx files are so complex, you will choose a third way, which is to write a simple program to add nontext-formatted resources to a .resources file. I show you how to write the program later in this section.

Because .resx files are so complex, why are they included? They are what Visual Studio 2005 uses to handle resources. In fact, you will use them quite extensively when you look at globalization and localization later in this chapter, but you will probably not even be aware that you are.

Building Text Name/Value Pair Resource Files

The simplest type of resource that you can create is the string table. You will probably want to create this type of resource using name/value pair files, as the format of the name/value pair file maps quite nicely to a string table. Basically, the name/value pair file is made up of many lines of name and value pairs separated by equal signs (=). Here is an example:

Name = Stephen Fraser

Email Address = stephen.fraser@apress.com

Phone Number = (502) 555-1234

Favorite Equation = E=mc2

As you can see, spaces are allowed for both the name and the value. Also, the equal sign can be used in the value (but not the name), as the first equal sign is used to delimit the name and the value.

Caution Don’t try to line up the equal signs, because the spaces will become part of the name. As you’ll see later in the chapter, doing this will make it harder to code the resource accessing method.

C H A P T E R 1 8 A S S E M B L Y P R O G R A M M I N G

757

ResGen

The text file you created previously is only an intermediate file. You might think of it as a source file just like a .cpp or .h file. You need to convert it to a .resources file so that your program will be able to process it as a resource. (By the way, you could process the file as a standard string file, but then you would lose many of the resource features provided by the .NET Framework.) To convert your text file, use the .NET Framework’s ResGen.exe utility. There is not much to running the utility:

> ResGen filename.txt

When you run the preceding code, assuming that the text file consists of valid name/value pairs, you get an output file of filename.resources in the directory where you ran the utility. You can work with these files as separate entities, or you can embed them into your assembly. You will see how to do that later in this chapter.

One more thing, if you are a glutton for punishment and write resource files using .resx files, then you would use the ResGen utility to convert them into .resources files as well.

ResourceWriter

As I stated previously, adding nontext resources is not possible using name/value pair files, and the

.resx file is a bear to work with. So what are you to do if you simply need to create nontext resources (e.g., an image table)?

You can use the System::Resources::ResourceWriter class, because this class has the capability to place almost any type of data within a .resources file, so long as the total combined size of the file does not exceed 2GB. In fact, this class is what ResGen.exe uses to generate its .resources file. Why they didn’t make ResGen.exe more robust and allow other types of data types escapes me.

Using the ResourceWriter class requires you to perform only three steps:

1.Open up a .resources file using the ResourceWriter class’s constructor.

2.Add resources to the .resources file using the AddResources() method.

3.Close the .resources file using the Close() method.

Listing 18-11 presents all the code you need to add an image to a .resources file from a .jpg file.

Listing 18-11. Adding an Image to a .resources File

#using <System.Drawing.dll> // Add the reference as it's not a default

using namespace System;

using namespace System::Resources; using namespace System::Drawing;

void main()

{

ResourceWriter ^rwriter = gcnew ResourceWriter("filename.resources"); rwriter->AddResource("ImageName", Images::FromFile("Imagefile.jpg")); rwriter->Close();

}