As you make the transition to .NET, you will probably find yourself using COM objects within .NET more than .NET objects within a COM project.
For the first lesson this hour, you will create a simple COM object using ATL and use the object within a managed C++ application. The first step is to create the ATL project and implement the COM object. Create a new project by selecting New, Project from the File menu. Select Visual C++ Projects from the list of project types and select the ATL Project template in the list of project templates. Give your project the name ATLCOMServer and click OK to create the project. Accept the default project settings by clicking Finish in the ATL Project Wizard.
The COM object you will be creating is the beginning of a simple system information object. It contains one interface that, in turn, contains one method and one property. Click Project, Add Class on the main menu. In the Add Class dialog that's displayed, select the ATL Simple Object template, as shown in Figure 21.2, and close the dialog by clicking Open.
In the ATL Simple Object Wizard, enter the name SimpleSysInfo in the Short Name field, as shown in Figure 21.3. Accept all the default options for this object by clicking Finish.
As mentioned earlier, your object will contain a single method. This method will be used to retrieve the current name of the machine the object is being run on by returning the results of the GetComputerName WIN32 API function. To add a method to an interface, expand the ATLCOMServer project in the Class View tree. Next, expand the Interfaces item. You should see the ISimpleSysInfo interface listed, as shown in Figure 21.4. Right-click this interface and select Add, Add Method in the context menu.
When the Add Method Wizard is displayed, give your method the name GetComputerName. Because interface functions return an HRESULT and you also need to return the computer name back to the caller, you have to create an out parameter. In the Parameter Type combo box, select the BSTR* parameter type. This will enable the Out and Retval check box under the Parameter Attributes heading. Check the Out and Retval check box. By doing this, you are creating a parameter that is going to be used to return a result back to the caller. In other words, the caller will pass you a buffer that you, in turn, will fill with the name of the computer. Give your parameter the name sComputerName and click the Add button to add the parameter to the parameter list, as shown in Figure 21.5. Click Finish to create the new interface method.
After you click Finish to create the method, the IDE will open the SimpleSysInfo.cpp file for you (if it isn't already open) and automatically scroll down to your new method. This method, as already mentioned, will use the GetComputerName function contained within the WIN32 API. However, because we are working within COM, you cannot simply pass the BSTR variable to the GetComputerName function because it expects a pointer to a regular character string. However, because you are using ATL, you can take advantage of the string-conversion macros it contains. Before you can use any of the ATL string-conversion macros, you must first insert the USES_CONVERSION macro, as shown in Listing 21.1, at the beginning of your function block. This macro is responsible for the variables and function calls necessary to perform the appropriate conversions.
1: STDMETHODIMP CSimpleSysInfo::GetComputerName(BSTR* sComputerName) 2: { 3: USES_CONVERSION; 4: 5: if( !sComputerName ) 6: return E_POINTER; 7: 8: DWORD dwSize = MAX_COMPUTERNAME_LENGTH + 1; 9: TCHAR sTemp[ MAX_COMPUTERNAME_LENGTH + 1 ]; 10: 11: if( !::GetComputerName( sTemp, &dwSize ) ) 12: return E_FAIL; 13: 14: *(sComputerName) = T2BSTR( sTemp ); 15: 16: return S_OK; 17: }
Following the USES_CONVERSION macro, line 5 of Listing 21.1 checks to make sure the caller-supplied buffer is valid and, if not, returns an HRESULT failure code. Next, declare the necessary local variables and call the GetComputerName function. Notice that because your interface method has the same name as the GetComputerName API call, you will need to preface the function call with the scope resolution operator (::). Finally, using the ATL string-conversion macro, which converts a native character string into a BSTR, assign the result to the caller-supplied buffer and return, as shown on line 14 of Listing 21.1. Although you currently have no way of testing the functionality of your object yet, it would be a good idea to compile your project before continuing.
Now you are going to add a property to the object. This property will actually perform the same functionality as the method you just added. Note that you would normally not have a property and function that perform the same tasks, but this is for illustration purposes only. Right-click the ISimpleSysInfo interface in Class View like you did earlier, but this time select Add, Add Property. In the Add Property Wizard, select the BSTR property type and give your property the name ComputerName, as shown in Figure 21.6. Click Finish to close the dialog.
As it did when you added a new method, the IDE will open the SimpleSysInfo.cpp file and scroll to the functions you just created. A property can contain a get function and a put function. Also, because you accepted the defaults when you created the property, the wizard has generated both functions for you. All that is left to do is implement these functions. The get_ComputerName function has the exact same signature (other than the name of the function, of course) as the interface function you added earlier. Because they both serve the same purpose to return the computer name you can simply copy the implementation for the GetComputerName function and place it within the get_ComputerName function. However, because the parameter name is different, you will have to change the code accordingly, as shown in Listing 21.2.
The put_ComputerName function is used to change the computer name. Because this is just a lesson and you're not creating a real shipping COM object, you might not want to implement this function. Listing 21.2 returns E_NOTIMPL, which is an HRESULT failure code signifying that the function is not implemented.
1: STDMETHODIMP CSimpleSysInfo::get_ComputerName(BSTR* pVal) 2: { 3: USES_CONVERSION; 4: 5: if( !pVal ) 6: return E_POINTER; 7: 8: DWORD dwSize = MAX_COMPUTERNAME_LENGTH + 1; 9: TCHAR sTemp[ MAX_COMPUTERNAME_LENGTH + 1 ]; 10: 11: if( !::GetComputerName( sTemp, &dwSize ) ) 12: return E_FAIL; 13: 14: *(pVal) = T2BSTR( sTemp ); 15: 16: return S_OK; 17: } 18: 19: STDMETHODIMP CSimpleSysInfo::put_ComputerName(BSTR newVal) 20: { 21: // not implemented 22: 23: return E_NOTIMPL; 24: }
Now that you have a working COM object, you can now learn how to use it within a managed application. Before you create the managed C++ application, however, you must change some of the build properties of the COM object to enable COM Interop. As mentioned at the beginning of this hour, COM Interop is accomplished by creating wrappers around the object you want to interoperate with. In this case, you will be creating a runtime callable wrapper (RCW) by running a utility provided by Visual Studio .NET on the type library that's generated whenever your COM DLL is built. This utility is called TlbImp.exe. Don't let the name confuse you, though. Although it sounds like it imports a type library, it doesn't. Rather, it creates a separate DLL, which is an assembly that runs within the common language runtime (CLR). This assembly contains the RCW that your .NET client will access. The RCW then performs the necessary marshaling and calls the COM object acting on the .NET client's behalf.
You can set up the TlbImp.exe tool to run each time you build your project so that you don't have to manually run it each time. In Class View, right-click the ATLCOMServer project and select Properties from the context menu. In the ATLCOMServer Property Pages dialog that's displayed, select Build Events, Post-Build Events from the list on the left side of the dialog. Select All Configurations from the Configuration drop-down box, as shown in Figure 21.7, because you want to build the .NET assembly in both Debug and Release modes. By doing this, you are specifying a tool you want to run after your project has been built.
In the Command Line field, you should already see a tool being run the regsvr32 tool. This tool is responsible for registering your COM object in the Registry. Click in the Command Line field and then click the button with the ellipsis (…) displayed at the end of the field. This will bring up the Command Line dialog, which allows you to specify more than one command. Add a carriage return after the regsvr32 command to begin a new command. Enter TlbImp.exe and the parameters shown in Figure 21.8. The first command-line argument specifies the DLL from which you want to extract type information. The second command-line argument is optional but recommended. It tells the tool to create a new DLL rather than overwrite the existing one. The macros within the dollar signs can be found by clicking the Macros button, selecting the appropriate macro, and then clicking the Insert button. For this project, use the same DLL name with the letters ASM at the end to make a distinction between the two files. Click OK to close the Command Line dialog.
In order to avoid any path issues with your COM object DLL while you run your .NET application, click the Linker item on the left side of the Configuration dialog. Change the Output File field on the right to ../bin/ATLCOMServer.DLL. When you create your .NET application, the executable for the project will be placed in the same location. You can now close the Property Pages dialog and build your COM object project. You shouldn't have any compilation errors, but because you have no way of testing the object, you cannot check for logical errors yet.
Now you can create the managed .NET application that will use the COM object you just created. Select New, Project from the main menu. Select Visual C++ Project from the list of project types and select the Managed C++ Application project template. Make sure the Add to Solution radio button is selected. Then, give your new project the name ManagedClient and click OK to create the project.
The TlbImp.exe tool created an assembly that can be used within managed applications. Because this is the case, you don't need to do anything else to set up the communication between your .NET client and the COM object. It behaves just like all the other assemblies and classes contained within those assemblies. Therefore, in the ManagedClient.cpp file, import the ATLCOMServerASM.dll file with the #using keyword, as shown in Listing 21.3. The assembly that was created also creates a default namespace that contains the interfaces and their associated coclasses. By default, this namespace is the same name as the DLL without the file extension. Therefore, add the appropriate namespace declaration, as shown on line 9 of Listing 21.3
1: #include "stdafx.h" 2: 3: #using <mscorlib.dll> 4: #using <..\bin\ATLCOMServerASM.dll> 5: 6: #include <tchar.h> 7: 8: using namespace System; 9: using namespace ATLCOMServerASM; 10: 11: void ComputerNameTest() 12: { 13: // create interop class 14: CSimpleSysInfoClass *SysInfo = new CSimpleSysInfoClass; 15: 16: // get machine name 17: String *sComputerName = SysInfo->GetComputerNameA();18: 18: 19: Console::WriteLine( "Computer Name as method: {0}", sComputerName ); 20: 21: // access ComputerName property 22: Console::WriteLine( "Computer Name as property: {0}", 23: SysInfo->ComputerName ); 24: 25: // attempt to set the computer name 26: try 27: { 28: SysInfo->ComputerName = S"vcsmarkhsch3"; 29: } 30: catch( Exception* e) 31: { 32: Console::WriteLine( "Setting computer name returned: \ 33: {0}\r\n", e->Message ); 34: } 35: } 36: 37: // This is the entry point for this application 38: int _tmain(void) 39: { 40: ComputerNameTest(); 41: 42: return 0; 43: }
Before we get into the implementation details, it's important to discuss what differences exist between the original COM object and the RCW assembly just created. When an assembly is created from a type library, the tool has to take into account the fundamental design differences between the two technologies. First of all, objects within the .NET Framework do not automatically contain an interface for IUnknown or IDispatch as they do for COM objects. Because they do not exist and there are no equivalent interfaces, the TlbImp tool simply removes those interfaces. Because IUnknown is used for object lifetime, it is not needed because object lifetime is contained and controlled by the common language runtime. For each coclass contained within a COM server, an equivalent class is created within the .NET assembly that is generated, and Class is appended to the coclass name.
In order to avoid the casting of interface pointers as you work with the new managed class and also to keep in line with the design of the .NET Framework, the RCW .NET assembly will flatten all interface members within a coclass. In other words, if a coclass implements several interfaces, the resulting .NET assembly will gather all the interface methods and properties for each implemented interface and place these within the managed class without using interfaces. Of course, one problem that is immediately apparent involves method and property name collisions. If one interface contains a method named A and another interface contains a method also named A, these two methods will collide when they are flattened within the managed class. To overcome this obstacle, the TlbImp tool will change the name of any colliding members by prefixing these members with the interface that implements them, followed by an underscore and the data member's original name. Therefore, in the example just mentioned, the first method will remain the same, A, whereas the next, colliding function will be renamed InterfaceName_A. Because the COM object you created does not implement more than one custom interface, no name collisions will occur. However, it is important to know the differences between the types within the COM object and the resulting RCW .NET assembly that is created.
Using the objects within the created RCW assembly is similar to using any other .NET Framework object. In the ManagedClient.cpp file, create a function named ComputerNameTest. It does not need to accept any parameters and can return void. To instantiate the class that represents the COM object, use the standard method of instantiating .NET objects with the C++ keyword new. This can be seen on line 14 of Listing 21.3, in which an object named CSimpleSysInfoClass is created. After creating the object, you can call its various methods and parameters, just as you would with any other .NET object. If you recall, when you created the COM object, you created an interface method named GetComputerName that accepts a BSTR as its parameter. However, because you are working within the CLR, there is no such data type as BSTR. Instead, use the equivalent data type, which is the System::String data type contained within the .NET Framework. The RCW will perform the necessary conversions from a String object to a BSTR, and vice versa.
Following the call to GetComputerName, print out the ComputerName property. Because you are not assigning the property to a value, the get_ComputerName interface method will be invoked. Although you did not implement the setter function for the ComputerName property, call it anyway. You can see, starting on line 28 of Listing 21.3, that the code to set the ComputerName property is wrapped within a try-catch exception block. The .NET Framework does not use HRESULT as COM does. Instead, the RCW will convert any HRESULT errors it receives by throwing exceptions instead. If you are returning E_NOTIMPL within the setter function of your ComputerName property, the exception message you receive reads, "Not implemented." If there is no equivalent exception within the .NET Framework's COM Interop namespace that can be mapped from an HRESULT, as is the case with a custom HRESULT, then a generic exception is thrown instead.
To finish your project, add the function call to ComputerNameTest within your _tmain function. Also, just as you did with the project properties of your COM object, change the Output File path to ../bin/ManagedClient.exe. Once you compile and run your application, your output should look similar to Figure 21.9.
Top |