Object Creation

Team-Fly    

 
.NET and COM Interoperability Handbook, The
By Alan Gordon
Table of Contents
Chapter Eight.  Advanced COM to .NET Interop

Object Creation

In Chapter 6, I looked at using managed components from a COM/Win32 client at a very high level. In that chapter, you saw that, in order to use a .NET component from a COM/Win32 client, you had to insert registry entries for the .NET component that are similar to the entries required by a regular COM object. You also learned in Chapter 6 that, in order for a .NET object to be creatable from a COM client, it must have a default constructor, that is, a constructor that takes no arguments. The reason for this restriction is that the CoCreateInstance methodand the other object activation methods in the COM API--do not provide any means to pass initialization parameters to a COM object. Therefore, if an unmanaged client instantiates a managed object through CoCreateInstance, it has no way to pass parameters to the object's constructor. If you are willing to do a little extra work, you can get around both of these restrictions. In this section, you learn how to create a .NET object from an unmanaged client without having to register the .NET object first. You also learn how to create a .NET object and pass parameters to its constructor.

Creating Objects Without Registering Them First

The CLR execution engine (MSCOREE.DLL) exports a function called ClrCreateManagedInstance that you can use to create a COM object that has not been registered using the Assembly Registration Tool (regasm.exe). When you call CoCreateInstance (or a similar COM activation method), the COM runtime takes the CLSID that you pass to it and uses it to look up the path to the executable or DLL that implements the CLSID beneath the HKEY_CLASSES_ROOT\CLSID\{CLSIDForComponent} key in the registry. When you use the Assembly Registration Tool to register an assembly, it inserts an entry beneath HKEY_CLASSES_ROOT\CSLID\{CLSIDForComponent} that points to the CLR execution engine (MSCOREE.DLL) and then puts a value beneath that key that identifies the managed class that the CLR execution engine should load after it starts. The ClrCreateManagedInstance function takes an assembly name and the name of a class within the assembly to load. ClrCreateManagedInstance then loads the execution engine and uses the CLR's standard assembly search algorithm to find the assembly and load the specified class. Unfortunately, you cannot call the ClrCreateManagedInstance function from Visual Basic. To use the ClrCreateManagedInstance function, you must first reference the mscoree type library (mscoree.tlb) from a Visual C++ application using the #import keyword, which is part of the native COM support in Visual C++. You can find the mscoree type library at: D:\WINNT\Microsoft.NET\Framework\v1.0.3705 . This is the correct version number for the release version of the .NET Framework, but your version number may vary. You can also use the mscoree include file ( mscoree.h ) or the mscoree IDL file ( mscoree.idl ) at D:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\include\ .

The following code shows Visual C++ logic that creates an instance of the .NET version of the financial component and calls the MonthlyPayment method. You do not need to register the assembly with the Assembly Registration Tool (regasm.exe) in order for this code to work.

 1.  void CTestClrCreateInstanceDlg::OnTestObjectButton() 2.  { 3.      HRESULT hRes; 4.      DISPID dispid; 5.      float fltResult; 6.      CString strPayment; 7.      OLECHAR *methodName=L"MonthlyPayment"; 8.      EXCEPINFO errorInfo; 9.      UINT intArg; 10.     VARIANT vntArgs[3], vntResult; 11.     DISPPARAMS param; 12.     param.cArgs=3; 13.     param.rgvarg=vntArgs; 14.     param.cNamedArgs=0; 15.     param.rgdispidNamedArgs=NULL; 16.     vntResult.vt=VT_R4; 17.     vntArgs[0].vt=VT_R4; 18.     vntArgs[0].fltVal=360000; 19.     vntArgs[1].vt=VT_R4; 20.     vntArgs[1].fltVal=6.75; 21.     vntArgs[2].vt=VT_I2; 22.     vntArgs[2].iVal=360; 23.     IDispatch *pObject; 24. 25.     hRes=::CoInitializeEx(NULL,COINIT_MULTITHREADED); 26.  27.     hRes=ClrCreateManagedInstance(   28.          L"DotNetFinancial.TimeValue,DotNetFinancial,   29.          Version=1.2.4.0,Culture=Neutral,   30.          PublicKeyToken=8a707be49fd7d8f4",   31.          IID_IDispatch,(void **)&pObject);  32. 33.     hRes=pObject->GetIDsOfNames(IID_NULL,&methodName, 34.           1,GetUserDefaultLCID(),&dispid); 35. 36.     hRes=pObject->Invoke(dispid,IID_NULL, 37.         GetUserDefaultLCID(),DISPATCH_METHOD, 38.         &param,&vntResult,&errorInfo,&intArg); 39. 40.     VarR4FromDec(&vntResult.decVal, &fltResult); 41.     strPayment.Format("$%.2f",fltResult); 42.     AfxMessageBox(strPayment); 43. 44.     if (pObject != NULL) 45.         pObject->Release(); 46. } 

In this code, you are late binding to the .NET Financial component using the IDispatch interface. Lines 3 through 11 are variable declarations. One of the most important of these declarations is on line 7 where you declare a string variable that contains the name of the method you plan to call (MonthlyPayment). Lines 12 through 22 initialize the DISPPARAMS structure that will hold the parameters that you will pass to the method. On line 25, you enter the MTA. So far, this code is identical to the code that you would write to access any COM component through the IDispatch interface. The most important lines of code in this example are lines 27 through 31. On these lines, you call the CLRCreateManagedInstance method to create an instance of the .NET Financial component. The CLRCreateManagedInstance method has three parameters. The first parameter contains the complete name of the class and the full name of the assembly where the class resides encoded as a string with the following format:

 "Namespace.TypeName,[FullAssemblyName]" 

The FullAssemblyName is the usual textual representation of a string name that includes the friendly name, version number, culture, and public key token of the assembly. The CLRCreateManagedInstance method will use the standard assembly search algorithm, so you don't need to register the assembly using regasm.exe. However, the assembly will either have to reside in the same directory as its client, or you will need to place it in the GAC. The remaining lines of code simply make a late-bound call to the MonthlyPayment method.

Remember that you need to include the header file for the runtime execution engine (mscoree.h) header file in order to compile the previous code. You also need to link with the link library for the execution library (mscoree.lib). If you are using Visual Studio .NET, you don't have to do any additional work to make all of this happen. The include and library directories for the .NET Framework SDK are included in the list of directories that Visual Studio .NET searches in for include and library files, respectively. If you are building your client with Visual Studio 6, you need to make some configuration changes before the previous code will compile.

First, you need to add the include directory for the .NET Framework SDK to the list of directories that Visual Studio 6 will search for include files. To do this, perform the following steps:

  1. Select Tools Options and then click the Directory tab.

  2. Select Include files from the Show directories for: drop-down list.

  3. Click the New Entry button (the left-most button directly to the left of the red X).

  4. Enter the location for the .NET Framework SDK include files. On my machine, it is D:\PROGRAM FILES\MICROSOFT VISUAL STUDIO .NET\FRAMEWORKSDK\INCLUDE (see Figure 8-1).

    Figure 8-1. Setting the Include directories on the Options dialog.

    graphics/08fig01.jpg

Also, make sure that you add the include file for the CLR execution engine to the top of your source file as shown here.

 #include "mscoree.h" 

Next, you need to add the .NET Framework SDKs library directory to the list of directories that Visual Studio 6 will search for libraries. To do this, perform the following steps:

  1. Select Tools Options and then click the Directory tab.

  2. Select Library files from the Show directories for: drop-down list.

  3. Click the New Entry button.

  4. Enter the location for the .NET Framework SDK library files. On my machine, it is D:\PROGRAM FILES\MICROSOFT VISUAL STUDIO .NET\FRAMEWORKSDK\LIB (see Figure 8-2).

    Figure 8-2. Setting the Library directories on the Options dialog.

    graphics/08fig02.jpg

Next you must add the link library for the CLR execution engine (mscorlib.lib) to the list of modules that the application will link with. To do this, perform the following steps:

  1. Select Project Settings. The Project Settings dialog will appear.

  2. Click the Link tab and enter mscoree.lib in the Object/library modules: field (see Figure 8-3).

    Figure 8-3. Add the MsCoree library to the project settings.

    graphics/08fig03.jpg

If you are using Visual Studio 6 and you want to use the CoInitializeEx method, you also need to add the following the following code to stdafx.h :

 #define _WIN32_WINNT 0x0400 

If you are using Visual Studio .NET, you do not need to perform this step. This last step has nothing to do with using .NET, but it is required to compile the code listing shown previously.

Hosting the CLR

Another way to access managed components from unmanaged code without having to register your assembly first is to host the CLR. The execution engine for the CLR is a DLL that can be hosted in any process. After you host the CLR, you can create application domains and load and run managed assemblies. You can find documentation on hosting the CLR at the following location: [Visual Studio .NET Install Location]\FrameworkSDK\Tool Developers Guide\docs\Hosting Interfaces.doc . On my machine, the exact location is: D:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Tool Developers Guide\docs\Hosting Interfaces.doc .

One big advantage of hosting the CLR is that you can call nondefault constructors, that is, constructors that have arguments. The steps to hosting the CLR are as follows :

  1. Create an instance of the CorRuntimeHost class.

  2. Call the Start method on the object.

  3. Create a new application domain (AppDomain object) or call GetDefaultDomain to get the default application domain.

  4. Call the CreateInstance method on the AppDomain class to instantiate managed objects.

  5. Call methods on the managed objects.

  6. Call stop on the CorRuntimeHost instance when you are done.

You can host the CLR from a Visual Basic or a Visual C++ application. If you are using Visual Basic, you will need to reference the CLR execution engine 1.0 library (mscoree.tlb), which you can find at the following location: [.NET Framework SDK Install Dir]\v[Version#]\mscoree.tlb. On my machine, this file resides at D:\WINNT\Microsoft.NET\Framework\v1.0.3705\ mscoree.tlb .

If you do not see an entry called Common Language Runtime Execution Engine 1.0 Library when you select Project References, just click the Browse button and navigate to the location where the file resides on your machine. Because the CLR execution engine uses types from the CLR library, you also need to reference type library for the Common Language Runtime Library, which you can find at the following location: [.NET Framework SDK Install Dir]\v[Version#]\mscorlibee.tlb . This file resides at the following location on my machine: D:\WINNT\Microsoft.NET\Framework\ v1.0.3705\mscorlib.tlb .

The following code shows how to host the CLR from a Visual Basic program:

 1.  Private Sub Command1_Click() 2.      Dim clr As CorRuntimeHost 3.      Dim domain As AppDomain 4.      Dim params(3) As Object 5.      Dim objFinancial As Object 6.      Dim fltResult As Single 7.      Set clr = New CorRuntimeHost 8.      clr.Start 9.      clr.GetDefaultDomain domain 10.     Set objFinancial = domain.CreateInstance(11.       "DotNetFinancial, 12.       Version=1.2.4.0,Culture=Neutral, 13.       PublicKeyToken=8a707be49fd7d8f4", 14.         "DotNetFinancial.TimeValue").Unwrap 15.     fltResult = objFinancial.MonthlyPayment(16.       360, 6.75, 360000) 17.     clr.Stop 18.     lblPayment.Caption = fltResult 19. End Sub 

Lines 1 through 6 contain variable declarations; the most important of these is the declaration of a CorRuntimeHost reference on line 2. On line 7, you create an instance of the CorRuntimeHost class. On line 8, you call the Start method on this object. Even though the documentation on hosting the CLR states that calling Start and Stop on the CorRuntimeHost object is not strictly necessary, I found that, if you omitted the Start and Stop methods, the previous code would only work correctly if you ran it from within the Visual Basic 6 IDE. If you build the executable without the Start and Stop methods, you always get a Catastrophic failure error when you run the application from the ".exe" file. On line 9, you call the GetDefaultDomain method on the CorRuntimeHost class to fetch an AppDomain object that references the default domain. Notice that the domain is returned as an output parameter to this method. On line 10, you call the CreateInstance method on the AppDomain object to create an instance of the TimeValue class in the DotNetFinancial namespace. The first parameter to the CreateInstance method is the full name of the assembly that contains the class. You must specify a full name if the assembly resides in the GAC. If the assembly is private, you can load it with a partial name. If you were using a private assembly, you could replace lines 10 through 14 shown previously with the following code:

 Set objFinancial = domain.CreateInstance("DotNetFinancial", "DotNetFinancial.TimeValue").Unwrap 

The only exception to this rule is the mscorlib assembly. You can create instances of classes in this library by specifying a partial name even though it resides in the GAC.

Regardless of whether you use a full or partial name to reference the type name, DotNetFinancial.TimeValue must be qualified with its namespace, and the type name is case sensitive. Notice that you call the Unwrap method on the return value of the Unwrap method. The CreateInstance method returns an object handle (an instance of the Mscorlib.ObjectHandle class). You must call the Unwrap method on this object in order to get a reference to a callable object.

If you are using Visual C++, you can use the CreateInstance_3 in the _AppDomain interface to instantiate an object that has a parameterized constructor.

Note

CreateInstance_3 is actually the name for one of the overloads of the CreateInstance method on the AppDomain class. See the section in this chapter entitled "Overloaded Methods" for an explanation of why this has such an odd name.


In this case, you are using the CreateInstance_3 method to instantiate the manager class that you created in Chapter 3. (I repeated the definition of the class here for your convenience.)

 namespace AssemblyDemo {         public class Manager : Employee         {           public Manager(int id,string name,               decimal salary,decimal bonus) :               base(id,name,salary)           {             this.mBonus=bonus;           }           public override decimal GetSalary()           {             return base.GetSalary()+mBonus;           }           private System.Object[] _array;           private decimal mBonus;         } } 

You cannot call the Create_Instance_3 method from Visual Basic because it uses by-value array parameters, which Visual Basic does not support.

 1.  void CVcpphostclrDlg::OnTestmanagedclassButton() 2.  { 3.      HRESULT hRes; 4.      ICorRuntimeHost *pRuntimeHost; 5.      IUnknown *pUnkAppDomain; 6.      _AppDomain *pAppDomain; 7.      _ObjectHandle *pHandle; 8.      OLECHAR *methodName=L"GetSalary"; 9.      VARIANT vntResult; 10.     float fltVal; 11.     DISPID dispid; 12.     EXCEPINFO errorInfo; 13.     UINT intArg; 14.     DISPPARAMS param; 15.     BSTR bstrTypeName, bstrAssemblyName; 16.     SAFEARRAY *paramArray; 17.     VARIANT paramID, paramName; 18.     VARIANT paramSalary, paramBonus; 19.     LONG index; 20.     hRes=CoInitializeEx(NULL,COINIT_MULTITHREADED); 21.     if (FAILED(hRes)) 22.         AfxMessageBox("Could not initialize COM"); 23.     hRes=CoCreateInstance(CLSID_CorRuntimeHost, 24.         NULL,CLSCTX_INPROC_SERVER, 25.         IID_ICorRuntimeHost, 26.         (void **)&pRuntimeHost); 27.     if (FAILED(hRes)) 28.         AfxMessageBox("Could not create host"); 29.     hRes=pRuntimeHost->Start(); 30.     hRes=pRuntimeHost->GetDefaultDomain(31.         &pUnkAppDomain); 32.     if (FAILED(hRes)) 33.         AfxMessageBox("Could not get Appdomain"); 34.     hRes=pUnkAppDomain->QueryInterface(35.         IID__AppDomain,(void **)&pAppDomain); 36.     if (FAILED(hRes)) 37.         AfxMessageBox("Failed to get domain"); 38.     if (pUnkAppDomain!=NULL) 39.         pUnkAppDomain->Release(); 40.     bstrTypeName=SysAllocString(41.         L"AssemblyDemo.Manager"); 42.     bstrAssemblyName=SysAllocString(43.         L"multifile3,Version=2.0.0.0,Culture=neutral, 44.           PublicKeyToken=8a707be49fd7d8f4"); 45.     paramArray=SafeArrayCreateVector(VT_VARIANT,0,4); 46. 47.     VariantInit(&paramID); 48.     paramID.vt=VT_I4; 49.     paramID.lVal=45; 50.     index=0; 51.     hRes=SafeArrayPutElement(paramArray, 52.         &index,&paramID); 53.     VariantInit(&paramName); 54.     paramName.vt=VT_BSTR; 55.     paramName.bstrVal=SysAllocString(L"Alan Gordon"); 56.     index=1; 57.     hRes=SafeArrayPutElement(paramArray, 58.         &index,&paramName); 59.     VariantInit(&paramSalary); 60.     paramSalary.vt=VT_R4; 61.     paramSalary.fltVal=1000.0; 62.     VariantChangeType(&paramSalary,&paramSalary, 63.         0,VT_DECIMAL); 64.     index=2; 65.     hRes=SafeArrayPutElement(paramArray, 66.         &index,&paramSalary); 67.     VariantInit(&paramBonus); 68.     paramBonus.vt=VT_R4; 69.     paramBonus.fltVal=100.0; 70.     VariantChangeType(&paramBonus,&paramBonus, 71.         0,VT_DECIMAL); 72.     index=3; 73.     hRes=SafeArrayPutElement(paramArray, 74.         &index,&paramBonus); 75.     hRes=pAppDomain->CreateInstance_3(76.         bstrAssemblyName,bstrTypeName,VARIANT_TRUE, 77.         BindingFlags_Default,NULL,paramArray, 78.         NULL,NULL,NULL,&pHandle); 79.     if (FAILED(hRes)) 80.         AfxMessageBox("Could not create object"); 81.     VARIANT vntObject; 82.     IDispatch *pObject; 83.     VariantInit(&vntObject); 84.     pHandle->Unwrap(&vntObject); 85.     vntObject.punkVal->QueryInterface(IID_IDispatch, 86.         (void **)&pObject); 87.     if (vntObject.punkVal!=NULL) 88.         vntObject.punkVal->Release(); 89. 90.     param.cArgs=0; 91.     param.rgvarg=NULL; 92.     param.cNamedArgs=0; 93.     param.rgdispidNamedArgs=NULL; 94.     VariantInit(&vntResult); 95.     hRes=pObject->GetIDsOfNames(IID_NULL, 96.         &methodName,1,GetUserDefaultLCID(),&dispid); 97.     hRes=pObject->Invoke(dispid,IID_NULL, 98.         GetUserDefaultLCID(), 99.         DISPATCH_METHOD,&param,&vntResult, 100.        &errorInfo,&intArg); 101.    if (SUCCEEDED(hRes)) 102.    { 103.        vntResult.decVal; 104.        VarR4FromDec(&vntResult.decVal,&fltVal); 105.    } 106.    else 107.    { 108.    // Handle the error, code omitted... 109.    } 110.    if (pObject!=NULL) 111.        pObject->Release(); 112.    if (pAppDomain!=NULL) 113.        pAppDomain->Release(); 114. 115.    SysFreeString(bstrTypeName); 116.    SysFreeString(bstrAssemblyName); 117.    SafeArrayDestroy(paramArray); 118. } 

Lines 3 through 19 contain variable declarations. On line 20, you enter the multithreaded apartment. Lines 23 through 26 create an instance of the CorRuntimeHost class and request the ICorRuntimeHost interface. On line 29, you call the start method, and, on line 30, you get the default appdomain. So far, the code that you have written is identical to the Visual Basic code that you wrote earlier to host the runtime. The difference starts on line 45 where you create a SafeArray of variants that will contain the parameters you will pass to the constructor. Lines 47 through 74 initialize each argument by first calling the VariantInit function and then initializing the fields of each variant. You then call the SafeArrayPutElement method to insert each parameter into the SafeArray. Lines 75 through 78 call the CreateInstance_3 method and pass in the full assembly name and the name of the type that you wish to instantiate for the first two parameters. The third, fourth, and fifth parameters are, respectively, as follows:

  • Boolean specifying whether the type name is case sensitive or not

  • Flag indicating whether to do a case-sensitive search for a constructor

  • Binder object that affects how you bind, coerce and retrieve argument types

The sixth parameter is the array of parameters that you wish to pass to the method. The seventh, eighth , and ninth arguments are the culture, the activation attributes, and the security attributes. The tenth parameter is an output parameter that contains the return value for the method, which is an object handle. Lines 81 through 88 turn the object handle into an IDispatch pointer, and then, lines 90 through 100 make a late-bound call to the GetSalary method on the Manager class. Lines 101 through 105 process the return value into a floating-point variable, and lines 110 through 117 contain cleanup code.


Team-Fly    
Top
 


. Net and COM Interoperability Handbook
The .NET and COM Interoperability Handbook (Integrated .Net)
ISBN: 013046130X
EAN: 2147483647
Year: 2002
Pages: 119
Authors: Alan Gordon

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net