All of the sample code presented to you in this chapter has been managed code that compiles to intermediate language. Some of it calls unmanaged code in the C runtime library, but the assembly produced in all the sample projects is 100% IL. When you combine managed and unmanaged code, you must keep in mind that there is a slight performance cost to call unmanaged code from managed code. One way to optimize the performance of your application is to move the boundary between managed and unmanaged code. Consider the system represented in Figure 6.4, with three classes. Class A must be written in managed code (for example, it's a garbage-collected public class that will be exposed to other code running on the runtime) and makes one call into Class B. Class B could be written in managed or unmanaged code. It makes multiple calls to Class C, which is an existing unmanaged class, part of a legacy library. Figure 6.4. A class that is called by managed code, and calls unmanaged code, can be written in either.
If Class B is written in managed code, your application will pay the performance cost of calling unmanaged code from managed code hundreds of times each time the application is run, as calls go from Class B to Class C. Alternatively, if Class B is written in unmanaged code, that performance penalty will only be incurred once, when Class A calls Class B. After that, the calls from Class B to Class C will all be unmanaged-to-unmanaged. Clearly, writing Class B in unmanaged code will make your application faster. Ideally, you could write Class B in unmanaged code using the same tool you use for Class A, and with the two classes in the same project. You can do just that with Visual Studio .NET 2003. In this section, you see how to compile one source file to unmanaged code in a project that builds to managed code. Using ILDASM to Explore AssembliesYou can examine the contents of an assembly using a tool called ILDASM, intermediate language dissassembler. The easiest way to start ILDASM is from a Visual Studio command prompt. Go to Start, Programs, Visual Studio .NET 2003, Visual Studio .NET Tools, and then Visual Studio .NET 2003 command prompt. Type ildasm at the prompt to start the tool, and open the release version of ManagedMathLibrary.dll. Figure 6.5 shows this assembly in ILDASM. The ManagedMathLibrary namespace has been expanded to show the two classes, Arithmetic and DLL , and each class has been expanded to show its methods. Static methods are marked with a yellow S in the pink square before each method name . Figure 6.5. ILDASM shows the contents of the ManagedMathLibrary assembly.
If you double-click a method, such as Add() or Subtract() , you can see the intermediate language for the method in a new window. This intermediate language is similar to assembly language. The IL for Subtract is simple: .method public instance float64 Subtract(float64 num1, float64 num2) cil managed { // Code size 6 (0x6) .maxstack 2 IL_0000: ldarg.1 IL_0001: ldarg.2 IL_0002: sub IL_0003: br.s IL_0005 IL_0005: ret } // end of method Arithmetic::Subtract Even with no assembly language background, you might be able to guess what the numbered lines do:
The IL for Add is a little more complex, because it has calls to the << operator, but it is still intermediate language. It makes calls to unmanaged code without being unmanaged code. A managed class, with the __gc keyword, can only be defined in managed code. A source file that doesn't define any managed classes can be written in either managed or unmanaged code, as you prefer. Consider the Simple class, defined in the same project as Arithmetic . It's not a garbage-collected class and it's not in a namespace. It has a constructor and a getx() method, which have been defined as in-line functions. Here's the class definition again: class Simple { private: int x; public: Simple(int xx): x(xx){}; int getx() {return x}; }; Because all the member functions of this class have been defined in-line, they don't appear in the disassembly of the intermediate language. They'll be expanded into the code that uses them and compiled in that location. To serve as a demonstration of unmanaged data in managed code, Simple can be changed so that the functions are no longer defined in-line. The header file, Simple.h, becomes class Simple { private: int x; public: Simple(int xx); int getx() ; }; The implementation file, Simple.cpp, becomes #include "StdAfx.h" #include "simple.h" #using <mscorlib.dll> Simple::Simple(int xx): x(xx) { } int Simple::getx() { return x; } When you build this project, the assembly contains IL for the constructor and the getx() function. You have to look hard for them among all the other methods from the CRT that are displayed in the ILDASM tool, but Simple.__ctor and Simple.getx are listed. The IL for the constructor looks like this: [View full width]
It basically just stores the argument that was passed to it. The first line is long and strange because this is a constructor, and specifically a constructor for a value type. Turning Off /clr for a Single FileWhy does Simple compile to intermediate language? Because the compiler was directed to compile it in that way. The directive is available on the property pages for the project as a whole, but also for each individual file. You can change the directive to compile to intermediate language for Simple.cpp while the rest of the project remains managed code by following these steps:
At this point you have changed the Simple.cpp properties so that it does not compile to intermediate language, does not use precompiled headers, and does not automatically bring in all the assemblies used by the project. But because this project has references to other assemblies, there is still work to do. Turning off the Force #using option does not override references. As a result, you must convert references to #using directives. You can do this file-by-file in each source file of the project, but it's more maintainable to use the Force #using property of the project. Here's how to do so:
Now build the project. There should be no errors or warnings. All the files in the project except Simple.cpp are using system.dll and mscorlib.dll and compiling to IL. Simple.cpp is not using any assemblies or precompiled headers, and is compiling to native code. By just declaring a class and function without using them, you leave the opportunity for the optimizer to not bother putting them into the assembly. Add some lines to the Add method in Arithmetic that use Simple , so that it looks like this: System::Double Add(System::Double num1, System::Double num2) { Simple s(2); cout << endl << "Simple is " << s.getx() << endl; cout << endl << "I am in Add" << endl; return num1 + num2; } Build the project, and open the assembly in ILDASM. Find the Simple member functions, and double-click them to examine the code. Instead of the IL that was shown to you when you disassembled the Simple constructor before, you will see something like this: [View full width]
This constructor has compiled to native code, not IL. You have succeeded in creating a class written in unmanaged code within a project that creates managed code. There are several important applications for this technique. One is to create a wrapper for a complex legacy class with a "chatty" interface. Like Class C in Figure 6.4, many legacy classes expose 10 or 20 functions, each setting a single parameter, and then expose another function that actually does something using those parameter values. An unmanaged wrapper class would have one method that takes all the parameters at once, and makes multiple calls to the legacy object, setting all the parameters and then calling the action function. This would reduce the impact of the switch from managed to unmanaged code, because the switch would happen only once instead of tens or hundreds of times. Remember, this capability is unique to Visual C++. |